.NET Native AOT .NET 9 Performance Minimal APIs Cloud Containers

Native AOT no .NET 9: APIs com startup em milissegundos e binário mínimo

Guia prático de Native AOT no .NET 9: diferença entre JIT e AOT, como publicar uma Minimal API com PublishAot, limitações com reflection e serialização.

N
Neryx Digital Architects
3 de janeiro de 2026
12 min de leitura
230 profissionais leram
Categoria: Arquitetura Público: Times de plataforma e operação Etapa: Aprendizado

Uma Minimal API .NET publicada com Native AOT inicia em menos de 10 milissegundos e ocupa menos de 15 MB em disco. A mesma API com o runtime JIT tradicional leva 200-400ms para iniciar e carrega um runtime de 60-80 MB. Em ambientes serverless, Kubernetes com autoescaling agressivo, ou containers com cold start frequente, essa diferença é real e impacta custo e latência.

Native AOT não é magia — é uma troca deliberada: você abre mão de algumas funcionalidades que dependem de reflection dinâmica em troca de startup ultra-rápido, menor uso de memória e binário autocontido sem dependência de runtime instalado.

JIT vs AOT: a diferença fundamental

O modelo tradicional do .NET compila C# para IL (Intermediate Language) no build, e o runtime JIT (Just-In-Time) compila IL para código nativo da máquina quando o código é executado pela primeira vez. Isso permite otimizações dinâmicas baseadas no hardware real e suporte a reflection completo, mas significa que cada processo começa com um período de aquecimento onde o JIT ainda está compilando.

O Native AOT compila tudo para código nativo no momento do build — no ambiente de CI/CD, não na máquina do usuário. O resultado é um binário executável que não depende do runtime .NET instalado, não tem JIT warm-up, e tem uso de memória significativamente menor. A desvantagem é que algumas funcionalidades que dependem de inspeção de tipos em runtime — reflection não anotada, serialização dinâmica, carregamento de assemblies — não funcionam ou exigem anotações extras.

Habilitando Native AOT na Minimal API

<!-- MyApi.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>

    <!-- Habilita Native AOT na publicação -->
    <PublishAot>true</PublishAot>

    <!-- Remove debugging info para binário menor em produção -->
    <StripSymbols>true</StripSymbols>

    <!-- Habilita invariant globalization (menor footprint, sem dados de cultura) -->
    <InvariantGlobalization>true</InvariantGlobalization>
  </PropertyGroup>
</Project>
// Program.cs — Minimal API compatível com AOT
// Regra principal: sem reflection dinâmica. Tudo deve ser conhecido em compile time.

using System.Text.Json.Serialization;

var builder = WebApplication.CreateSlimBuilder(args);  // CreateSlimBuilder: versão AOT-friendly

// System.Text.Json com Source Generation — necessário para AOT
// Gera código de serialização em compile time, sem reflection em runtime
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});

builder.Services.AddScoped<IOrderRepository, OrderRepository>();

var app = builder.Build();

var ordersGroup = app.MapGroup("/api/orders");

ordersGroup.MapGet("/{id:guid}", async (Guid id, IOrderRepository repo) =>
{
    var order = await repo.GetByIdAsync(id);
    return order is null ? Results.NotFound() : Results.Ok(order);
});

ordersGroup.MapPost("/", async (CreateOrderRequest request, IOrderRepository repo) =>
{
    var order = await repo.CreateAsync(request);
    return Results.Created($"/api/orders/{order.Id}", order);
});

ordersGroup.MapGet("/", async (IOrderRepository repo) =>
    Results.Ok(await repo.GetAllAsync()));

app.Run();

// JsonSerializerContext com Source Generation — OBRIGATÓRIO para AOT
// Lista explicitamente todos os tipos que serão serializados/desserializados
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(CreateOrderRequest))]
[JsonSerializable(typeof(List<Order>))]
[JsonSerializable(typeof(IEnumerable<Order>))]
internal partial class AppJsonContext : JsonSerializerContext { }
# Publicação AOT para Linux x64 (típico para containers)
dotnet publish -r linux-x64 -c Release

# Resultado em publish/MyApi:
# Binário único, autocontido, ~12-18 MB
# Não requer .NET runtime instalado no container

# Dockerfile para imagem mínima
# Dockerfile AOT — imagem final sem runtime .NET
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src

# Instala ferramentas necessárias para cross-compilation
RUN apt-get update && apt-get install -y clang zlib1g-dev

COPY ["MyApi.csproj", "."]
RUN dotnet restore

COPY . .
RUN dotnet publish -r linux-x64 -c Release -o /app/publish

# Imagem final: apenas o binário, sem runtime .NET
FROM mcr.microsoft.com/dotnet/runtime-deps:9.0
WORKDIR /app
COPY --from=build /app/publish .

EXPOSE 8080
ENTRYPOINT ["./MyApi"]

Limitações e como contorná-las

Reflection dinâmica: qualquer código que use Type.GetMethod(), Activator.CreateInstance() ou similar sem anotação AOT vai falhar em runtime. A solução é o atributo [DynamicallyAccessedMembers] que informa ao compilador AOT quais membros serão acessados, ou reescrever o código para evitar reflection.

System.Text.Json sem Source Generation: o JsonSerializer usa reflection para descobrir propriedades. Com AOT, você precisa do JsonSerializerContext como mostrado acima — o compilador gera código de serialização em build time.

AutoMapper e bibliotecas similares: AutoMapper usa reflection extensivamente e não é compatível com AOT sem configuração especial. A alternativa é Mapperly (usa Source Generators) ou mapeamento manual.

Entity Framework Core: EF Core tem suporte experimental a AOT no .NET 9, mas com restrições. Para APIs com AOT, Dapper é frequentemente mais prático — é mais simples e tem menos dependência de reflection.

// Exemplo com Dapper em API AOT — simples e sem friction
public class OrderRepository : IOrderRepository
{
    private readonly string _connectionString;

    public OrderRepository(IConfiguration config)
        => _connectionString = config.GetConnectionString("Default")!;

    public async Task<Order?> GetByIdAsync(Guid id)
    {
        await using var conn = new NpgsqlConnection(_connectionString);
        // Dapper usa reflection para mapeamento, mas de forma compatível com AOT
        // em .NET 9 quando os tipos são conhecidos
        return await conn.QueryFirstOrDefaultAsync<Order>(
            "SELECT * FROM orders WHERE id = @id AND is_deleted = false",
            new { id });
    }
}

Benchmark: JIT vs AOT em números reais

Métrica JIT (.NET 9) Native AOT (.NET 9) Diferença
Startup time ~250ms ~8ms 30x mais rápido
Tamanho do binário ~80MB (com runtime) ~14MB 6x menor
Uso de memória (idle) ~60MB RSS ~25MB RSS 2.4x menor
Throughput (req/s) Baseline (após warm-up) ~95-98% do JIT Praticamente igual
Latência p99 (estável) Baseline ~105% do JIT Levemente pior

O throughput e latência em estado estacionário são praticamente equivalentes entre JIT e AOT. O JIT tem vantagem em workloads de longa duração porque pode fazer otimizações adaptativas baseadas em padrões de execução reais. O AOT tem vantagem clara em cold start e footprint de memória.

Quando Native AOT compensa

Compensa:** serverless functions (AWS Lambda, Azure Functions) onde cold start impacta diretamente a latência do usuário; microsserviços com autoescaling frequente que passam por muitos cold starts; CLIs e ferramentas que precisam iniciar rapidamente; ambientes com memória muito restrita (IoT, edge computing).

Geralmente não compensa: APIs monolíticas com instâncias sempre ativas (o JIT eventualmente supera o AOT em throughput após warm-up); projetos que dependem fortemente de reflection (ORMs com proxies, containers de DI com convenções dinâmicas, serialização polimórfica complexa); quando a equipe não tem familiaridade com as limitações AOT e o custo de migração supera o benefício.

O .NET Aspire suporta Native AOT — se você usa o AppHost para orquestrar seus serviços, a migração de um serviço para AOT é transparente para os outros serviços na topologia.


Decidir entre JIT e AOT depende do perfil de workload do seu produto. Se você quer uma análise técnica dos trade-offs para o seu contexto específico, a Neryx pode ajudar.

Precisa desenhar a próxima fase com menos retrabalho?

Fazemos discovery técnico para mapear riscos, arquitetura-alvo e sequência de execução antes de investir pesado.

Solicitar Discovery

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.