Advanced C# AOT Compilation: Complete Guide with Examples and Tutorials

⚡ Ahead-of-Time (AOT) vs Just-in-Time (JIT) Compilation

🐚
JIT (Traditional .NET)
  • ✓ Dynamic code generation
  • ✓ Full reflection support
  • ✓ Large BCL available
  • ✗ Slow startup (100ms-5s)
  • ✗ Large memory footprint
  • ✗ JIT warmup overhead
🚀
AOT (Native AOT)
  • ✓ 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.

Post a Comment

Previous Post Next Post