A compilação Native AOT (Ahead-of-Time) é uma das evoluções mais impactantes do ecossistema .NET. No .NET 10, ela atingiu maturidade suficiente para ser considerada em cenários de produção reais — APIs, workers, CLI tools e microserviços de alta performance.

O que é Native AOT?

No modelo tradicional, aplicações .NET são compiladas para IL (Intermediate Language) e convertidas em código nativo pelo JIT (Just-in-Time) durante a execução. Com AOT, essa conversão acontece em tempo de build — o resultado é um binário nativo, sem dependência do runtime .NET.

Tradicional: C# → IL → JIT (runtime) → código nativo
Native AOT:  C# → IL → AOT (build)   → binário nativo

O binário gerado é self-contained — não precisa do .NET instalado na máquina de destino.

Por que considerar AOT no .NET 10?

O .NET 10 trouxe avanços significativos que tornam o AOT viável para cenários que antes eram impraticáveis:

  • Startup até 10x mais rápido — sem warm-up do JIT
  • Consumo de memória reduzido em 30-60% — sem metadata do runtime
  • Binários menores — tree-shaking agressivo remove código não utilizado
  • Sem dependência do runtime — deploy de um único executável
  • Suporte completo a Minimal APIs — o cenário mais maduro para APIs web
  • Melhorias no trimming — menos warnings e mais bibliotecas compatíveis

Habilitando AOT em um projeto

Adicione a propriedade PublishAot ao seu .csproj:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <PublishAot>true</PublishAot>
  </PropertyGroup>
</Project>

Publique com:

dotnet publish -c Release -r linux-x64

O output é um binário nativo único — sem .dll, sem dotnet CLI, sem runtime.

Exemplo: Minimal API com AOT

O template ideal para AOT é uma Minimal API enxuta:

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});

var app = builder.Build();

app.MapGet("/health", () => Results.Ok(new HealthResponse("ok", DateTime.UtcNow)));

app.MapGet("/products/{id:int}", (int id) =>
{
    var product = ProductStore.GetById(id);
    return product is not null ? Results.Ok(product) : Results.NotFound();
});

app.Run();

record HealthResponse(string Status, DateTime Timestamp);

[JsonSerializable(typeof(HealthResponse))]
[JsonSerializable(typeof(Product))]
internal partial class AppJsonContext : JsonSerializerContext { }

O ponto-chave: source generators para JSON

O AOT não suporta reflection em runtime da mesma forma que o JIT. Por isso, a serialização JSON precisa de source generators — o JsonSerializerContext gera o código de serialização em tempo de compilação.

Isso é o que o AppJsonContext faz no exemplo acima. Para cada tipo que você quer serializar/deserializar, adicione um [JsonSerializable(typeof(...))].

Comparativo: JIT vs AOT no .NET 10

MétricaJIT (padrão)Native AOT
Startup (cold)~300ms~30ms
Memória (idle)~80MB~25MB
Tamanho do deploy~120MB~15MB
Throughput (rps)~95k~90k
Tempo de build~5s~30s
Reflection completa⚠️ Limitada
Dynamic loading

Nota: Os valores são aproximados e variam conforme o projeto. O throughput sustentado do JIT pode ser ligeiramente superior após warm-up, pois o JIT otimiza hot paths em runtime.

O que funciona bem com AOT

Minimal APIs

O cenário mais maduro. WebApplication.CreateSlimBuilder() já é otimizado para AOT:

var builder = WebApplication.CreateSlimBuilder(args);

// ✅ Funciona perfeitamente com AOT
app.MapGet("/api/data", () => new { message = "AOT ready" });
app.MapPost("/api/items", (Item item) => Results.Created($"/api/items/{item.Id}", item));

gRPC

gRPC usa code generation, o que é naturalmente compatível com AOT:

builder.Services.AddGrpc();
app.MapGrpcService<GreeterService>();

Workers e background services

Ideal para workers que processam filas ou executam tarefas agendadas:

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<QueueProcessor>();
var host = builder.Build();
host.Run();

CLI tools

Ferramentas de linha de comando se beneficiam enormemente do startup rápido:

# Com JIT: ~200ms para um "hello world"
# Com AOT: ~5ms

O que NÃO funciona (ou requer cuidado)

Reflection dinâmica

APIs que usam Type.GetType(), Activator.CreateInstance() ou Assembly.Load() em runtime não funcionam com AOT:

// ❌ Não funciona com AOT
var type = Type.GetType("MyNamespace.MyClass");
var instance = Activator.CreateInstance(type);

// ✅ Alternativa: usar generics ou DI
services.AddSingleton<IMyService, MyService>();

Entity Framework Core

O EF Core tem suporte parcial a AOT no .NET 10. Queries compiladas funcionam, mas migrations e o tooling requerem o runtime completo:

// ✅ Funciona com AOT (query compilada)
public static readonly Func<AppDbContext, int, Task<Product?>> GetById =
    EF.CompileAsyncQuery((AppDbContext db, int id) =>
        db.Products.FirstOrDefault(p => p.Id == id));

// ⚠️ Queries LINQ dinâmicas podem ter limitações

Bibliotecas com reflection pesada

  • AutoMapper → use Mapperly (source generator)
  • MediatR → funciona com source generators a partir da v13
  • FluentValidation → suporte parcial, preferir validação manual ou source generators

Docker com AOT

A grande vantagem no Docker: você não precisa da imagem do runtime. Use runtime-deps ou até scratch:

# === STAGE 1: Build com AOT ===
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -r linux-x64 -o /app/publish

# === STAGE 2: Runtime mínimo ===
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled
WORKDIR /app
COPY --from=build /app/publish .
USER $APP_UID
EXPOSE 8080
ENTRYPOINT ["./MinhaApi"]

Resultado: imagem final com ~15MB em vez de ~220MB com o runtime completo.

Para o menor tamanho possível, use uma imagem scratch:

FROM scratch
COPY --from=build /app/publish/MinhaApi /
ENTRYPOINT ["/MinhaApi"]

Otimizações de tamanho do binário

Adicione estas propriedades ao .csproj para reduzir o tamanho:

<PropertyGroup>
    <PublishAot>true</PublishAot>
    <StripSymbols>true</StripSymbols>
    <OptimizationPreference>Size</OptimizationPreference>
    <InvariantGlobalization>true</InvariantGlobalization>
    <IlcGenerateStackTraceData>false</IlcGenerateStackTraceData>
</PropertyGroup>
PropriedadeEconomiaTrade-off
StripSymbols~30%Sem símbolos para debug
OptimizationPreference~15%Pode reduzir throughput
InvariantGlobalization~20%Sem suporte a culturas/locales
Stack trace data~10%Stack traces menos detalhadas

Lidando com trimming warnings

O AOT depende do trimming para remover código não utilizado. Warnings de trimming indicam código que pode quebrar em runtime:

dotnet publish -c Release -r linux-x64 | grep "IL2"

Os warnings mais comuns:

  • IL2026 — membro requer análise de código não compatível com trimming
  • IL2072 — tipo passado para parâmetro com requisitos de trimming
  • IL2104 — assembly com atributo de compatibilidade indefinido

Suprimindo warnings (com cuidado)

// Apenas se você tem certeza de que o código é seguro
[UnconditionalSuppressMessage("Trimming", "IL2026")]
public void MetodoSeguro() { }

Melhor abordagem: resolver na fonte

// ❌ Reflection dinâmica gera warning
var props = typeof(MyClass).GetProperties();

// ✅ Source generator resolve o problema
[JsonSerializable(typeof(MyClass))]
internal partial class MyJsonContext : JsonSerializerContext { }

Quando usar AOT?

Use AOT quando:

  • Startup rápido é crítico — serverless (Azure Functions, AWS Lambda), CLI tools
  • Memória é limitada — containers com limites baixos, IoT, edge computing
  • Deploy simplificado — single binary sem dependência de runtime
  • Microserviços pequenos — APIs com poucas rotas e lógica focada
  • Alta densidade de containers — mais instâncias por nó com menos RAM

Mantenha JIT quando:

  • Usa reflection extensiva — ORMs complexos, serializers dinâmicos
  • Precisa de dynamic loading — plugins, assemblies carregados em runtime
  • Throughput sustentado é prioridade — o JIT otimiza hot paths após warm-up
  • Projeto grande com muitas dependências — o build AOT pode ser lento
  • Time não tem experiência com trimming — debugging de trimming issues é complexo

Migrando um projeto existente para AOT

Se você tem uma API JIT e quer migrar, siga este passo a passo:

1. Teste a compatibilidade

dotnet publish -c Release -r linux-x64 /p:PublishAot=true

Analise os warnings. Se houver muitos, considere uma migração gradual.

2. Substitua reflection por source generators

Priorize JSON serialization e validação:

// Adicione ao Program.cs
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});

3. Adicione ao .csproj

<PublishAot>true</PublishAot>

4. Teste localmente

dotnet publish -c Release -r linux-x64 -o ./publish
./publish/MinhaApi

5. Valide em staging

  • Teste todos os endpoints
  • Verifique serialização/deserialização
  • Monitore memória e CPU
  • Compare latência com a versão JIT

Conclusão

O Native AOT no .NET 10 não é mais experimental — é uma opção de produção para cenários específicos. A combinação de startup instantâneo, consumo mínimo de memória e deploy simplificado faz dele a escolha ideal para microserviços, serverless e CLI tools.

A chave é entender os trade-offs: se seu projeto depende de reflection dinâmica ou EF Core completo, o JIT ainda é a melhor opção. Mas se você pode trabalhar com source generators e Minimal APIs, o AOT entrega performance que antes só era possível com Go ou Rust.

Quer otimizar a performance das suas aplicações .NET? A DevPlus tem experiência em arquitetura de alta performance e migração para AOT. Fale com a gente.