Skip to content

Advanced C# AOT Compilation: An Educational Guide with Examples

Advanced C# AOT Compilation: An Educational Guide with Examples

English original published: 2026-05-16T18:58:25.751+03:00. Original English article

Corrected educational edition. Technical review against primary sources: 2026-10-28. Code examples were not compiled or runtime-tested. The original English publication date is preserved; the Hebrew edition uses its actual publication date, not the English original date.

Review constraints

Complete Guide with Examples and Tutorials

  1. C# Source

    .cs files

    Program.cs

    Roslyn

  2. IL / MSIL

    Bytecode

    .dll/.exe

    AOT

  3. Native Binary

    No JIT startup

    Memory varies

    Measure performance

AOT properties

  • Startup may improve
  • Memory varies
  • 📦 Self-contained
  • 🐳 Cloud-native
  • ☁️ Serverless
  • 🛡️ Security

.NET · C# · AOT · NativeAOT · Performance · Cloud Native

Original-format diagram (optional; scroll horizontally at full size)
Advanced C# AOT Compilation: An Educational Guide with Examples

Original English diagram

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

🐚
JIT (Traditional .NET)
  • ✓ Dynamic code generation
  • ✓ Full reflection support
  • ✓ Large BCL available
  • Startup depends on the application; JIT may add warmup cost source
  • Memory use depends on workload and deployment
  • ✗ JIT warmup overhead
🚀
AOT (Native AOT)
  • Startup can improve; measure your application source
  • Memory use can improve; measure it source
  • Self-contained native deployment; OS dependencies remain source
  • ✓ 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 rather than using JIT compilation during execution. The output is a self-contained native application for a specific OS/architecture. A separately installed .NET runtime is not required, but native system dependencies and supported platform/toolchain requirements still apply. Startup and memory benefits must be measured for the actual app. source

.NET offers multiple ahead-of-time deployment techniques; these two serve different goals:

  • Native AOT compiles application IL to native code at publish time and does not use JIT during execution. The .NET 10 example below is a supported-version illustration as of 2026-10-28, not a compatibility guarantee for every dependency or platform. source1 source2
  • ReadyToRun (R2R) precompiles some code but retains IL and can still use JIT; it normally increases published size and is not Native AOT. Evaluate its startup/size/compatibility trade-offs separately. source

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>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
    <OptimizationPreference>Speed</OptimizationPreference>
  </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 supports Native AOT for supported application patterns including Minimal APIs. Use the Web SDK and the relevant AOT template/configuration, and verify supported features and dependencies. The sample is not a measured microservice benchmark. source

// Separate ASP.NET Core project using Microsoft.NET.Sdk.Web and net10.0.
// Project settings: <PublishAot>true</PublishAot>
// Runtime-untested illustration; not a persistent product service.
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

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) =>
{
    // Demonstrates a response shape only: no persistence or unique-ID guarantee.
    var product = new Product(100, req.Name, req.Price);
    return TypedResults.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

Use DI patterns that the trimmer and AOT analyzer can understand, with explicit registrations where practical. Constructor injection alone does not guarantee AOT compatibility. The Microsoft DI example below creates a scope before resolving a scoped service and places top-level statements before type declarations. source

// Separate console example, with a compatible Microsoft.Extensions.DependencyInjection package.
// Runtime-untested. Top-level statements precede all type declarations.
using System;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddSingleton<IDataService, DataService>();
services.AddScoped<IReportService, ReportService>();
using var provider = services.BuildServiceProvider(validateScopes: true);
using var scope = provider.CreateScope();
var reporter = scope.ServiceProvider.GetRequiredService<IReportService>();
Console.WriteLine(reporter.GenerateReport());

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())}";
}

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:

// Separate console example. Runtime-untested.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

var order = new Order(1, "CUST-001",
    new List<OrderItem> { new(42, "Widget", 3, 9.99m) }, 29.97m, "Pending");
var json = JsonSerializer.Serialize(order, OrderJsonContext.Default.Order);
var parsed = JsonSerializer.Deserialize(json, OrderJsonContext.Default.Order);
Console.WriteLine(json);

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);
[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 { }

📊 AOT Performance: What to Measure

Metric JIT Native AOT Comparison method
Cold startup Measure target build Measure same workload Same host, repeated runs
Published application size State deployment mode Include required artifacts Compare equivalent contents
Memory at idle and load Measure RSS consistently Measure comparable phases Report variance and setup

Trimmer Attributes — Keeping AOT Safe

// Type-declaration example, not top-level statements. Runtime-untested.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

public static class FactoryExamples
{
    // A hazard declaration, NOT a fix for trimming or Native AOT compatibility.
    [RequiresUnreferencedCode("Type names and constructors may be removed by trimming.")]
    [RequiresDynamicCode("Arbitrary runtime types may require unavailable native code.")]
    public static object CreateFromTypeName(string typeName)
    {
        var type = Type.GetType(typeName, throwOnError: true)!;
        return Activator.CreateInstance(type)
            ?? throw new InvalidOperationException("Constructor returned no object.");
    }

    // Prefer explicit, statically visible factories for a closed set of types.
    private static readonly Dictionary<string, Func<object>> Factories = new()
    {
        ["user"] = () => new User("", 0, ""),
        ["product"] = () => new Product(0, "", 0)
    };
    public static object CreateFromName(string name) =>
        Factories.TryGetValue(name, out var factory) ? factory()
            : throw new ArgumentException($"Unknown type: {name}");

    // This annotation describes required members; callers must satisfy the requirement.
    public static void PrintProperties<
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T obj)
    {
        foreach (var prop in typeof(T).GetProperties())
        {
            if (prop.GetIndexParameters().Length == 0 && prop.GetMethod is not null)
                Console.WriteLine($"{prop.Name}: {prop.GetValue(obj)}");
        }
    }
    private record User(string Name, int Age, string Email);
    private record Product(int Id, string Name, decimal Price);
}

Conclusion

Native AOT can suit selected APIs, CLI tools, serverless functions and microservices, but suitability depends on supported features, dependencies and measured behavior. Move reflection-heavy paths toward source generation or explicit registration, resolve trimming/AOT warnings and test the published binary on each target. Do not treat RequiresUnreferencedCode as a safety fix: it declares a hazard rather than preserving missing types. No benchmark or cloud-cost saving has been demonstrated here. source1 source2

All examples are runtime-untested; no compiler, publish or benchmark was run. The corrected .NET 10 project uses OptimizationPreference=Speed instead of the original OptimizeSpeed property. InvariantGlobalization was removed because it changes culture-sensitive behavior, including behavior relevant to Hebrew; enable it only after a deliberate requirements review. Use the native compiler toolchain required by the target platform; the shown Windows/Linux publish commands are alternatives, not a promise of cross-OS compilation. Each tutorial is a separate example, not one concatenated source file.

Sources for corrections

Post a Comment

Previous Post Next Post