Home C# / .NET Native AOT Compilation in .NET: A Complete Guide
Advanced 3 min · July 13, 2026

Native AOT Compilation in .NET: A Complete Guide

Learn how to compile your .NET apps to native code with Native AOT.

N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 13, 2026
last updated
2,043
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of C# and .NET
  • .NET 8 SDK installed
  • Familiarity with command-line tools
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Native AOT compiles C# code directly to machine code, eliminating the need for a JIT compiler or runtime.
  • It produces a single-file executable with fast startup and low memory footprint.
  • Not all .NET features are supported; reflection and dynamic code generation are limited.
  • Ideal for cloud-native, serverless, and containerized applications.
  • Requires .NET 8+ and specific project configuration.
✦ Definition~90s read
What is Native AOT Compilation in .NET?

Native AOT is a compilation technology that converts your .NET application into a native executable, eliminating the need for a JIT compiler and runtime.

Imagine you have a recipe written in a language that needs a translator every time you cook.
Plain-English First

Imagine you have a recipe written in a language that needs a translator every time you cook. Native AOT is like translating the entire recipe into your native language once, so you can cook instantly without any translator. Your app becomes a standalone executable that runs directly on the operating system.

In the world of modern software development, performance and efficiency are paramount. Whether you're deploying microservices in Kubernetes, building serverless functions, or creating command-line tools, every millisecond of startup time and every megabyte of memory counts. Traditional .NET applications rely on a Just-In-Time (JIT) compiler that compiles Intermediate Language (IL) to machine code at runtime. While this provides flexibility and cross-platform compatibility, it introduces overhead. Enter Native AOT (Ahead-of-Time) compilation, a game-changer introduced in .NET 7 and refined in .NET 8 and beyond. Native AOT compiles your entire application, including the runtime, into a single native executable. This means no JIT compilation, no IL interpreter, and no runtime dependencies. The result? Blazing-fast startup times, lower memory usage, and smaller deployment artifacts. However, Native AOT comes with trade-offs: limited support for reflection, dynamic code generation, and certain runtime features. In this tutorial, we'll dive deep into Native AOT, exploring its benefits, limitations, and practical usage. You'll learn how to configure your projects, handle common pitfalls, and decide when to use Native AOT. By the end, you'll be equipped to leverage this powerful technology for your next high-performance .NET application.

What is Native AOT?

Native AOT (Ahead-of-Time) compilation is a technology that compiles .NET code directly to native machine code before execution, rather than using a Just-In-Time (JIT) compiler at runtime. This means the entire application, including the runtime, is compiled into a single executable file. The result is a self-contained binary that does not require the .NET runtime to be installed on the target machine. Native AOT is particularly beneficial for scenarios where startup time and memory footprint are critical, such as serverless functions, containers, and command-line tools. However, it comes with limitations: not all .NET features are supported. For example, dynamic code generation via System.Reflection.Emit is not available, and reflection is heavily restricted. The compiler performs aggressive trimming and static analysis to determine which types and members are actually used, so any code that relies on runtime discovery must be explicitly annotated.

HelloAot.csCSHARP
1
2
3
using System;

Console.WriteLine("Hello, Native AOT!");
Output
Hello, Native AOT!
🔥Did You Know?
📊 Production Insight
In production, Native AOT can reduce cold start times from seconds to milliseconds, which is crucial for serverless and containerized workloads.
🎯 Key Takeaway
Native AOT compiles your app into a single native executable, eliminating the need for a JIT compiler and reducing startup time.

Setting Up a Native AOT Project

To get started with Native AOT, you need .NET 8 SDK or later. Create a new console application and modify the project file to enable AOT. The key property is PublishAot, which must be set to true. Additionally, you should set the target runtime (e.g., win-x64, linux-x64) and enable trimming for smaller binaries. Here's a step-by-step guide: 1. Create a new console project: dotnet new console -n AotDemo. 2. Edit the .csproj file to add the necessary properties. 3. Publish the application using the dotnet publish command with the appropriate runtime identifier. The output will be a single executable file. Note that Native AOT is only supported for console applications and ASP.NET Core minimal APIs (experimental). Class libraries cannot be published as AOT directly, but they can be consumed by AOT applications if they are compatible.

AotDemo.csprojCSHARP
1
2
3
4
5
6
7
8
9
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <PublishAot>true</PublishAot>
    <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
    <SelfContained>true</SelfContained>
  </PropertyGroup>
</Project>
💡Pro Tip
📊 Production Insight
Always publish with -c Release to enable optimizations. Debug builds are not supported for AOT.
🎯 Key Takeaway
Enable Native AOT by setting PublishAot=true in your project file or via command-line flag.

Understanding Trimming and Reflection

One of the biggest challenges with Native AOT is reflection. The AOT compiler performs static analysis to determine which types and members are used. If your code uses reflection to access types that are not directly referenced, they may be trimmed away, causing runtime failures. To prevent this, you can use attributes like [DynamicDependency] or [RequiresUnreferencedCode] to inform the linker. Additionally, you can use a linker descriptor XML file to explicitly preserve types. Another approach is to use source generators to generate code at compile time instead of using reflection. For example, System.Text.Json source generator can be used to avoid reflection-based serialization. Let's look at an example where we use [DynamicDependency] to preserve a type used via reflection.

ReflectionExample.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.Diagnostics.CodeAnalysis;

public class MyClass
{
    public void MyMethod() => Console.WriteLine("Hello from MyMethod");
}

public class Program
{
    [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(MyClass))]
    public static void Main()
    {
        var type = Type.GetType("MyClass");
        var instance = Activator.CreateInstance(type);
        type.GetMethod("MyMethod").Invoke(instance, null);
    }
}
Output
Hello from MyMethod
⚠ Warning
📊 Production Insight
In production, avoid reflection-heavy libraries like AutoMapper or Newtonsoft.Json. Use source-generated alternatives (e.g., System.Text.Json source gen).
🎯 Key Takeaway
Use [DynamicDependency] or linker descriptors to preserve types accessed via reflection in AOT-compiled apps.

Native AOT with ASP.NET Core Minimal APIs

Starting from .NET 8, ASP.NET Core minimal APIs support Native AOT. This allows you to create high-performance web services with minimal startup time. However, not all ASP.NET Core features are compatible. For example, you cannot use MVC controllers, Razor Pages, or SignalR with AOT. Only minimal APIs are supported. Additionally, you must avoid dynamic features like custom model binding or runtime compilation. To create an AOT-compatible minimal API, you need to use the Microsoft.AspNetCore.App framework and ensure your code is trimmable. Here's an example of a simple minimal API that can be published with AOT.

Program.csCSHARP
1
2
3
4
5
6
var builder = WebApplication.CreateSlimBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello, World!");

app.Run();
🔥Note
📊 Production Insight
For production, ensure all middleware and dependencies are AOT-compatible. Test thoroughly with AOT publish before deploying.
🎯 Key Takeaway
ASP.NET Core minimal APIs can be published with Native AOT, but only a subset of features is supported.

Performance Benchmarks: JIT vs AOT

Native AOT excels in startup time and memory usage, but may have slightly slower steady-state performance compared to JIT because the JIT can optimize based on runtime profiling. However, for many applications, the difference is negligible. Let's compare a simple 'Hello World' console app published with JIT and AOT. The AOT version starts in under 10ms, while the JIT version takes around 100ms. The binary size for AOT is around 1-2 MB, while the JIT version with runtime is about 50 MB. For a more complex application, the gap narrows but AOT still wins on startup. Memory usage is also lower for AOT because the runtime is compiled into the binary and no JIT code is generated.

Benchmark.csCSHARP
1
2
3
4
5
6
using System.Diagnostics;

var sw = Stopwatch.StartNew();
Console.WriteLine("Hello, World!");
sw.Stop();
Console.WriteLine($"Elapsed: {sw.ElapsedMilliseconds} ms");
Output
Hello, World!
Elapsed: 2 ms
💡Benchmarking Tip
📊 Production Insight
For microservices that scale to zero, AOT's startup advantage is critical. For long-running services, JIT may be acceptable.
🎯 Key Takeaway
Native AOT provides faster startup and lower memory, but may have slightly lower peak throughput for CPU-bound workloads.

Limitations and Compatibility

Not all .NET features work with Native AOT. Here's a list of unsupported features: System.Reflection.Emit (dynamic code generation), COM interop, C++/CLI, runtime code generation (e.g., Expression.Compile), and certain reflection APIs. Additionally, some NuGet packages may not be AOT-compatible. Always check for AOT compatibility warnings when publishing. You can use the dotnet-trimmer tool to analyze your dependencies. Another limitation is that you cannot use AOT for class libraries; only executable projects can be published as AOT. However, you can create an AOT-hosted library by building a native host that loads the library. The table below summarizes the compatibility of common .NET features.

Unsupported.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// This code will fail with AOT
using System.Reflection.Emit;

var assemblyName = new AssemblyName("DynamicAssembly");
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
var typeBuilder = moduleBuilder.DefineType("DynamicType", TypeAttributes.Public);
var methodBuilder = typeBuilder.DefineMethod("Hello", MethodAttributes.Public, null, null);
var ilGenerator = methodBuilder.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldstr, "Hello from dynamic code!");
ilGenerator.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new[] { typeof(string) }));
ilGenerator.Emit(OpCodes.Ret);
var dynamicType = typeBuilder.CreateType();
var instance = Activator.CreateInstance(dynamicType);
dynamicType.GetMethod("Hello").Invoke(instance, null);
Output
Unhandled exception: System.PlatformNotSupportedException: System.Reflection.Emit is not supported on this platform.
⚠ Important
📊 Production Insight
If you rely on libraries that use reflection heavily (e.g., AutoMapper, Newtonsoft.Json), consider migrating to source-generated alternatives.
🎯 Key Takeaway
Native AOT does not support dynamic code generation, COM interop, or full reflection. Plan your architecture accordingly.

Advanced: Custom Linker Descriptors

When [DynamicDependency] is not enough, you can use a linker descriptor XML file to explicitly preserve types, methods, or assemblies. This is useful for complex scenarios where reflection is used across multiple assemblies. Create an XML file (e.g., LinkerConfig.xml) and set it as a LinkerDescriptor in your project file. The linker descriptor format allows you to specify assemblies, types, and members to keep. Here's an example that preserves all members of a specific assembly.

LinkerConfig.xmlCSHARP
1
2
3
4
5
<linker>
  <assembly fullname="MyLibrary">
    <type fullname="MyLibrary.MyClass" preserve="all"/>
  </assembly>
</linker>
💡Pro Tip
📊 Production Insight
Minimize the use of linker descriptors to keep binary size small. Prefer source generators for new code.
🎯 Key Takeaway
Linker descriptors provide fine-grained control over what types and members are preserved during AOT compilation.
● Production incidentPOST-MORTEMseverity: high

The 10-Second Startup That Cost $1000

Symptom
Users experienced frequent timeouts when invoking a serverless function, especially after periods of inactivity.
Assumption
The development team assumed the cold start was due to network latency or database connection overhead.
Root cause
The .NET application relied on JIT compilation, which caused a 10-second delay on cold start as the runtime compiled IL code.
Fix
Switched to Native AOT compilation, reducing cold start time to under 100ms.
Key lesson
  • Cold start is a critical metric for serverless and containerized apps.
  • Native AOT can dramatically reduce startup time by eliminating JIT.
  • Always profile startup performance in production-like environments.
  • Consider Native AOT for latency-sensitive services.
  • Test thoroughly because not all libraries support AOT.
Production debug guideSymptom to Action3 entries
Symptom · 01
Application crashes with 'MissingMethodException' or 'TypeLoadException' at runtime.
Fix
Check if the missing type or method is used via reflection. Add a DynamicDependency attribute or use a source generator.
Symptom · 02
Application fails to start with 'AOT analysis failed' error.
Fix
Review the build output for trimming warnings. Add RuntimeDirectives or use a linker descriptor file.
Symptom · 03
Memory usage is higher than expected.
Fix
Ensure you're not using dynamic code generation (e.g., Expression trees). Profile with dotnet-counters.
★ Quick Debug Cheat SheetCommon issues and immediate actions for Native AOT.
Reflection-based code fails
Immediate action
Add [DynamicDependency] or use source generators
Commands
dotnet publish -c Release -r win-x64 --self-contained
dotnet publish -c Release -r linux-x64 -p:PublishAot=true
Fix now
Replace reflection with compile-time code generation
Large binary size+
Immediate action
Enable trimming and optimize
Commands
dotnet publish -c Release -r linux-x64 -p:PublishTrimmed=true
dotnet publish -c Release -r linux-x64 -p:PublishAot=true -p:OptimizationPreference=Size
Fix now
Remove unused dependencies
Missing runtime features (e.g., dynamic code)+
Immediate action
Check compatibility list
Commands
dotnet publish -c Release -r linux-x64 -p:PublishAot=true --no-build
dotnet tool install -g dotnet-ildasm
Fix now
Rewrite using static code
FeatureJITNative AOT
Startup Time100-500 ms<10 ms
Memory UsageHigher (runtime + JIT)Lower (no runtime)
Binary Size~50 MB (with runtime)~1-2 MB
Reflection SupportFullLimited (must preserve)
Dynamic Code GenerationSupportedNot supported
Platform SupportAll .NET platformsWindows, Linux, macOS (x64/ARM64)
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
HelloAot.csusing System;What is Native AOT?
AotDemo.csprojSetting Up a Native AOT Project
ReflectionExample.csusing System;Understanding Trimming and Reflection
Program.csvar builder = WebApplication.CreateSlimBuilder(args);Native AOT with ASP.NET Core Minimal APIs
Benchmark.csusing System.Diagnostics;Performance Benchmarks
Unsupported.csusing System.Reflection.Emit;Limitations and Compatibility
LinkerConfig.xmlAdvanced

Key takeaways

1
Native AOT compiles your .NET app into a single native executable with fast startup and low memory.
2
Use [DynamicDependency] or linker descriptors to preserve types used via reflection.
3
Only console apps and ASP.NET Core minimal APIs support AOT; class libraries do not.
4
Test with AOT early to catch compatibility issues.
5
Prefer source generators over reflection for new code.

Common mistakes to avoid

3 patterns
×

Using reflection without preserving types

×

Assuming all NuGet packages work with AOT

×

Not testing with AOT publish until late in development

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is Native AOT and how does it differ from JIT compilation?
Q02SENIOR
How do you handle reflection in a Native AOT application?
Q03SENIOR
What are the main limitations of Native AOT in .NET?
Q01 of 03JUNIOR

What is Native AOT and how does it differ from JIT compilation?

ANSWER
Native AOT compiles .NET code to native machine code before execution, producing a self-contained executable. JIT compiles IL to machine code at runtime. AOT eliminates the need for a runtime, reduces startup time, and lowers memory usage, but limits dynamic features.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Can I use Native AOT with any .NET project?
02
Does Native AOT support all NuGet packages?
03
Is Native AOT faster than JIT?
04
How do I debug a Native AOT application?
N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 13, 2026
last updated
2,043
articles · all by Naren
🔥

That's C# Advanced. Mark it forged?

3 min read · try the examples if you haven't

Previous
C# 12-14 and .NET 9-10 Modern Features
17 / 22 · C# Advanced
Next
Collection Expressions and Frozen Collections