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étrica | JIT (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>
| Propriedade | Economia | Trade-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.