⚡ Ahead-of-Time (AOT) vs Just-in-Time (JIT) Compilation
- ✓ Dynamic code generation
- ✓ Full reflection support
- ✓ Large BCL available
- ✗ Slow startup (100ms-5s)
- ✗ Large memory footprint
- ✗ JIT warmup overhead
- ✓ Near-instant startup (<10ms)
- ✓ Small memory footprint
- ✓ Single self-contained binary
- ✓ No JIT dependencies
- ✗ No runtime code generation
- ✗ Limited reflection support
What is AOT Compilation in .NET?
Ahead-of-Time (AOT) compilation in .NET converts your C# code into native machine code before deployment — not at runtime like the traditional JIT (Just-In-Time) compiler. The result is a self-contained native executable that starts almost instantly, uses less memory, and requires no .NET runtime on the target machine.
.NET supports two types of AOT compilation:
- Native AOT (introduced in .NET 7, production-ready in .NET 8/9) — Compiles everything to native code. Smallest binaries, fastest startup.
- ReadyToRun (R2R) — Partial AOT. Precompiles IL to native code but keeps IL for flexibility. Good balance of startup speed and compatibility.
Tutorial 1: Your First Native AOT Application
# Create a new console app
dotnet new console -n AotDemo
cd AotDemo
Edit your AotDemo.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<PublishAot>true</PublishAot>
<OptimizeSpeed>true</OptimizeSpeed>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
</Project>
Write AOT-compatible code in Program.cs:
using System.Text.Json;
using System.Text.Json.Serialization;
// Native AOT requires source-generated JSON serialization
[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(List<User>))]
internal partial class AppJsonContext : JsonSerializerContext { }
record User(string Name, int Age, string Email);
class Program
{
static void Main(string[] args)
{
Console.WriteLine("AOT App Started!");
var users = new List<User>
{
new("Alice", 30, "alice@example.com"),
new("Bob", 25, "bob@example.com")
};
// Source-generated serialization - no reflection!
var json = JsonSerializer.Serialize(users, AppJsonContext.Default.ListUser);
Console.WriteLine(json);
var loaded = JsonSerializer.Deserialize(json, AppJsonContext.Default.ListUser);
foreach (var user in loaded!)
Console.WriteLine($"Name: {user.Name}, Age: {user.Age}");
}
}
// Publish commands:
// dotnet publish -r linux-x64 -c Release
// dotnet publish -r win-x64 -c Release
Tutorial 2: AOT-Compatible Minimal API
ASP.NET Core 8/9 supports Native AOT for minimal APIs. This creates tiny, blazing-fast microservices:
// .csproj additions needed:
// <PublishAot>true</PublishAot>
// <EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.TypeInfoResolverChain.Insert(0, ApiJsonContext.Default));
var app = builder.Build();
app.MapGet("/", () => "Hello from Native AOT API!");
app.MapGet("/products", () => new[]
{
new Product(1, "Laptop", 999.99m),
new Product(2, "Mouse", 29.99m)
});
app.MapPost("/products", (CreateProductRequest req) =>
{
var product = new Product(Random.Shared.Next(100, 999), req.Name, req.Price);
return Results.Created($"/products/{product.Id}", product);
});
app.Run();
record Product(int Id, string Name, decimal Price);
record CreateProductRequest(string Name, decimal Price);
[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(Product[]))]
[JsonSerializable(typeof(CreateProductRequest))]
internal partial class ApiJsonContext : JsonSerializerContext { }
Tutorial 3: Dependency Injection in AOT
Traditional DI containers rely on reflection. For Native AOT, use constructor injection explicitly or Microsoft.Extensions.DependencyInjection (which is AOT-compatible from .NET 8):
using Microsoft.Extensions.DependencyInjection;
interface IDataService { string[] GetData(); }
interface IReportService { string GenerateReport(); }
class DataService : IDataService
{
public string[] GetData() => new[] { "Item1", "Item2", "Item3" };
}
class ReportService : IReportService
{
private readonly IDataService _data;
public ReportService(IDataService data) => _data = data;
public string GenerateReport()
=> $"Report: {string.Join(", ", _data.GetData())}";
}
// AOT-compatible DI
var services = new ServiceCollection();
services.AddSingleton<IDataService, DataService>();
services.AddScoped<IReportService, ReportService>();
using var provider = services.BuildServiceProvider();
var reporter = provider.GetRequiredService<IReportService>();
Console.WriteLine(reporter.GenerateReport());
Tutorial 4: Source Generators for AOT JSON
The key to JSON serialization in AOT is the [JsonSerializable] source generator. Here is a comprehensive example for a REST API domain:
using System.Text.Json.Serialization;
// Domain models
public record OrderItem(int ProductId, string Name, int Quantity, decimal UnitPrice);
public record Order(int Id, string CustomerId, List<OrderItem> Items, decimal Total, string Status);
public record CreateOrderRequest(string CustomerId, List<OrderItem> Items);
// Generate serializers for ALL types used in your API
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(Order[]))]
[JsonSerializable(typeof(List<Order>))]
[JsonSerializable(typeof(OrderItem))]
[JsonSerializable(typeof(CreateOrderRequest))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
)]
public partial class OrderJsonContext : JsonSerializerContext { }
// Usage
var order = new Order(1, "CUST-001",
new List<OrderItem> { new(42, "Widget", 3, 9.99m) },
29.97m, "Pending");
// Serialize - no reflection, AOT safe
var json = JsonSerializer.Serialize(order, OrderJsonContext.Default.Order);
// Deserialize - no reflection, AOT safe
var parsed = JsonSerializer.Deserialize(json, OrderJsonContext.Default.Order);
📊 AOT Performance: Real Numbers
| Metric | JIT | Native AOT | Improvement |
|---|---|---|---|
| Cold Startup | ~400ms | <8ms | 50x faster |
| Binary Size (min API) | 180MB | ~12MB | 15x smaller |
| Memory (idle API) | ~45MB RSS | ~12MB RSS | 3.7x less |
Trimmer Attributes — Keeping AOT Safe
using System.Diagnostics.CodeAnalysis;
// Tell the trimmer this method uses reflection
[RequiresUnreferencedCode("Uses Type.GetType() which requires all types to be present")]
public object CreateFromTypeName(string typeName)
{
var type = Type.GetType(typeName)!;
return Activator.CreateInstance(type)!;
}
// Better pattern - use a factory dictionary instead
private static readonly Dictionary<string, Func<object>> _factories = new()
{
["user"] = () => new User("", 0, ""),
["product"] = () => new Product(0, "", 0)
};
public object CreateFromName(string name)
=> _factories.TryGetValue(name, out var factory)
? factory()
: throw new ArgumentException($"Unknown type: {name}");
// Inform trimmer about dynamically accessed members
public static void PrintProperties<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T obj)
{
foreach (var prop in typeof(T).GetProperties())
Console.WriteLine($"{prop.Name}: {prop.GetValue(obj)}");
}
Conclusion
Native AOT in .NET 8/9 is production-ready for APIs, CLI tools, serverless functions, and microservices. The startup time reduction (from hundreds of milliseconds to single-digit milliseconds) and memory savings make it transformative for cloud-native and serverless scenarios. The main investment is migrating away from reflection-heavy patterns toward source generators and explicit type registration. Start with simple console apps or minimal APIs, learn the trimmer warnings, and adopt source-generated JSON serialization — your future self (and your cloud bill) will thank you.