Como gerar a especificação OpenAPI em tempo de build no ASP.NET Core

Gerar o OpenAPI em tempo de build significa produzir o documento da sua API durante o dotnet build, e não apenas quando a aplicação está rodando. A especificação OpenAPI é um dos pilares da comunicação entre equipes que desenvolvem e consomem APIs modernas, por isso ter esse arquivo disponível cedo faz diferença.

No ecossistema .NET, a partir do .NET 9, o ASP.NET Core passou a oferecer suporte nativo à geração desses documentos. Com isso, você não depende mais de bibliotecas de terceiros como o Swashbuckle.

Neste artigo, você vai ver os pacotes oficiais da Microsoft, as opções de configuração disponíveis e os cenários em que essa abordagem traz vantagens concretas.

O que é OpenAPI e por que ele importa

OpenAPI é um padrão independente de linguagem para documentar APIs HTTP. A indústria adotou esse padrão em larga escala para descrever contratos de interface entre sistemas.

Com um documento OpenAPI válido, equipes geram clientes tipados automaticamente e fazem testes baseados em especificação. Além disso, publicam documentação interativa sem esforço manual.

Se você quer comparar as ferramentas de documentação disponíveis no .NET, o artigo sobre OpenAPI, Swagger e Scalar na prática faz esse panorama.

Runtime versus OpenAPI em tempo de build

Por padrão, o ASP.NET Core gera o documento OpenAPI em tempo de execução, e não em tempo de build. Ou seja, ele serve o arquivo por um endpoint HTTP enquanto a aplicação está no ar.

Essa abordagem é conveniente no desenvolvimento, mas tem limitações. Afinal, há contextos em que o JSON precisa existir antes de a aplicação rodar.

O OpenAPI em tempo de build resolve justamente esses casos. A documentação oficial cita três cenários principais:

  • Incluir o documento no controle de versão para facilitar revisões de código.
  • Usar o contrato em testes de integração baseados em especificação, dentro do pipeline de CI.
  • Servir o documento de forma estática por um servidor web, sem iniciar a aplicação.

Na prática, o contrato vira um artefato do build, assim como a DLL. Portanto, qualquer mudança na API aparece no diff do pull request.

Pré-requisitos

Para seguir os exemplos, você precisa do .NET 9 SDK ou superior instalado na máquina.

O projeto deve ser uma aplicação ASP.NET Core, com Minimal APIs ou com controllers. Além disso, o .csproj precisa usar o TargetFramework net9.0 ou superior.

Caso ainda não tenha familiaridade com o modelo, o artigo sobre Minimal APIs no ASP.NET cobre o básico antes de avançar.

Instalando o pacote principal

O pacote Microsoft.AspNetCore.OpenApi fornece a geração de documentos OpenAPI em tempo de execução. Então, o primeiro passo é adicioná-lo ao projeto:

dotnet add package Microsoft.AspNetCore.OpenApi

Com o pacote instalado, registre os serviços no container de injeção de dependência e mapeie o endpoint que serve o documento:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapGet("/", () => "Hello world!");

app.Run();

Ao executar a aplicação, o documento fica disponível em https://localhost:{port}/openapi/v1.json. Até aqui, porém, tudo acontece em runtime.

Habilitando o OpenAPI em tempo de build

Para gerar o OpenAPI em tempo de build, instale o pacote Microsoft.Extensions.ApiDescription.Server:

dotnet add package Microsoft.Extensions.ApiDescription.Server

Esse pacote deve ser referenciado com configurações específicas no .csproj. Dessa forma, ele funciona como dependência de build e não segue junto com a aplicação publicada:

<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.*" />
  <PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="9.0.*">
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    <PrivateAssets>all</PrivateAssets>
  </PackageReference>
</ItemGroup>

Com a instalação concluída, o dotnet build passa a gerar o documento automaticamente. Por padrão, o arquivo aparece na pasta obj do projeto com o nome {ProjectName}.json.

Você pode conferir o resultado com dois comandos:

dotnet build
cat obj/{ProjectName}.json

A propriedade OpenApiGenerateDocuments

Além do pacote, vale declarar de forma explícita a propriedade MSBuild OpenApiGenerateDocuments no .csproj:

<PropertyGroup>
  <OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>
</PropertyGroup>

Essa propriedade controla o passo de geração do Microsoft.Extensions.ApiDescription.Server durante o build. Por isso, guias como o de Gérald Barré e respostas no Microsoft Q&A recomendam deixá-la como true no projeto.

Declarar a propriedade também deixa a intenção clara para quem lê o .csproj. Assim, ninguém precisa adivinhar por que um JSON novo surgiu após o build.

Personalizando a saída do OpenAPI em tempo de build

O comportamento padrão funciona, mas raramente atende a um projeto real.

A documentação oficial do ASP.NET Core lista todas as opções disponíveis. Felizmente, o pacote expõe propriedades MSBuild para ajustar diretório, nome e documento gerado.

Personalizando o diretório de saída

Por padrão, o JSON vai para a pasta obj. Isso dificulta o commit no repositório e o uso direto em pipelines de CI/CD.

Para alterar o destino do OpenAPI em tempo de build, configure a propriedade OpenApiDocumentsDirectory no .csproj:

<PropertyGroup>
  <OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>
  <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>
</PropertyGroup>

O valor $(MSBuildProjectDirectory) aponta para a raiz do projeto. Assim, o arquivo nasce ao lado do .csproj, pronto para versionamento.

A documentação oficial mostra uma alternativa equivalente com o valor .. Nesse caso, o MSBuild resolve o caminho de forma relativa ao arquivo de projeto.

Personalizando o nome do arquivo gerado

O nome padrão do arquivo segue o nome do projeto, no formato {ProjectName}.json. Para mudar, use a propriedade OpenApiGenerateDocumentsOptions com o argumento --file-name:

<PropertyGroup>
  <OpenApiGenerateDocumentsOptions>--file-name openapi</OpenApiGenerateDocumentsOptions>
</PropertyGroup>

Com essa configuração, o arquivo gerado passa a se chamar openapi.json, independente do nome do projeto. Isso simplifica scripts de CI, que não precisam mais descobrir o nome do projeto.

Múltiplos documentos no OpenAPI em tempo de build

Em aplicações maiores, você pode precisar de documentos distintos para audiências ou versões diferentes. Por exemplo, um contrato público e outro interno.

O ASP.NET Core permite registrar vários documentos chamando AddOpenApi mais de uma vez, com nomes diferentes:

builder.Services.AddOpenApi("v1");
builder.Services.AddOpenApi("v2");

Por padrão, o gerador de OpenAPI em tempo de build emite arquivos para todos os documentos registrados. Quando o nome do documento for diferente de v1, o arquivo segue a convenção {ProjectName}_{DocumentName}.json.

Para gerar apenas um documento específico durante o build, use o argumento --document-name:

<PropertyGroup>
  <OpenApiGenerateDocumentsOptions>--document-name v2</OpenApiGenerateDocumentsOptions>
</PropertyGroup>

Separando endpoints por documento

O ASP.NET Core usa a propriedade GroupName do ApiDescription para decidir quais endpoints pertencem a cada documento.

Endpoints sem GroupName entram em todos os documentos. Por outro lado, endpoints com GroupName aparecem somente no documento cujo nome corresponde ao grupo:

app.MapGet("/public/products", () => "Public products")
   .WithGroupName("v1");

app.MapGet("/internal/reports", () => "Internal reports")
   .WithGroupName("v2");

Você também pode personalizar esse comportamento com um delegate próprio na propriedade ShouldInclude do OpenApiOptions. Dessa forma, aplica critérios de filtragem arbitrários sobre cada ApiDescription.

Controlando a inicialização no OpenAPI em tempo de build

O OpenAPI em tempo de build funciona iniciando o ponto de entrada da aplicação com uma implementação mock do servidor. Esse mock existe porque nem toda informação do documento pode ser obtida por análise estática.

Isso significa que toda a lógica do Program.cs executa durante o build. Ou seja, o registro de serviços no container de DI e a leitura de configurações também rodam.

Em cenários com banco de dados ou integrações externas, esse comportamento pode quebrar o build. Nesse caso, proteja esses trechos com uma verificação do assembly de entrada:

using System.Reflection;

var builder = WebApplication.CreateBuilder(args);

// evita registrar o banco quando o gerador de build executa o Program.cs
if (Assembly.GetEntryAssembly()?.GetName().Name != "GetDocument.Insider")
{
    builder.Services.AddDbContext<AppDbContext>(options =>
        options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
}

var app = builder.Build();
app.Run();

O nome GetDocument.Insider identifica o processo interno que o Microsoft.Extensions.ApiDescription.Server usa na geração. Portanto, quando esse é o assembly de entrada, você sabe que a aplicação não está atendendo requisições reais.

Conflitos com Swashbuckle

No OpenAPI em tempo de build, um ponto de atenção importante aparece na migração de projetos que usavam Swashbuckle.AspNetCore.SwaggerGen. Os dois geradores não convivem bem no mesmo projeto.

O Swashbuckle.AspNetCore.SwaggerGen e o Microsoft.Extensions.ApiDescription.Server competem pelo controle da geração. Segundo a resposta de um moderador no Microsoft Q&A, eles trazem alvos MSBuild e pipelines conflitantes.

Quando os dois pacotes estão presentes, nenhum documento sai do build. Pior ainda, não aparece nenhuma mensagem de erro clara.

A solução é remover o Swashbuckle.AspNetCore.SwaggerGen antes de usar o Microsoft.Extensions.ApiDescription.Server:

<!-- remova esta referência -->
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="8.x.x" />

Vale destacar que a interface visual do Swagger continua disponível. Afinal, você pode usar o Swagger UI sem o SwaggerGen, apontando para o documento gerado de forma nativa.

Logs do OpenAPI em tempo de build

Quando o dotnet build executa o passo GetDocument, as mensagens não aparecem na verbosidade padrão do Terminal Logger. Isso vale a partir do .NET 8.

Para ver os logs detalhados, aumente a verbosidade do Terminal Logger:

dotnet build -tlp:v=d

Outra opção é desativar o Terminal Logger e usar o formato de log tradicional:

dotnet build --tl:off

Esses comandos ajudam bastante quando o OpenAPI em tempo de build não aparece. Assim, você descobre se o passo rodou, falhou ou nem chegou a ser acionado.

Revisando o OpenAPI em tempo de build no pull request

Versionar o arquivo gerado muda a forma como o time revisa alterações de API. Afinal, quem aprova o pull request passa a ver o contrato mudando linha a linha.

Imagine que alguém renomeia uma propriedade de um DTO. Sem o OpenAPI em tempo de build, essa mudança passa despercebida até um cliente quebrar em produção.

Com o arquivo no repositório, o diff mostra a propriedade antiga removida e a nova adicionada. Dessa forma, o revisor percebe a quebra de contrato antes do merge.

Uma prática comum é falhar o pipeline quando o arquivo gerado difere do arquivo commitado. Por exemplo, um passo pode rodar git diff --exit-code openapi.json logo após o build.

Assim, o desenvolvedor precisa gerar e commitar o contrato atualizado junto com o código. Em outras palavras, o OpenAPI em tempo de build passa a funcionar como uma trava de qualidade.

Esse fluxo também beneficia quem consome a API. Equipes de front-end ou de outros serviços acompanham as mudanças pelo histórico do arquivo, sem depender de uma instância rodando.

Validando o OpenAPI em tempo de build no CI/CD

Com o documento gerado no build e versionado no repositório, você pode incluir uma etapa de validação no pipeline. Esse é um dos maiores ganhos do OpenAPI em tempo de build.

A ferramenta @scalar/cli valida documentos OpenAPI pela linha de comando. Ela se integra a pipelines do GitHub Actions ou do GitLab CI:

- name: Build and generate OpenAPI document
  run: dotnet build --configuration Release

- name: Validate OpenAPI document
  run: npx @scalar/cli document validate ./openapi.json

Essa abordagem detecta mudanças que quebram o contrato antes de chegarem à produção. Com isso, os deploys ganham mais confiabilidade.

Se o seu pipeline usa GitHub Actions, o artigo sobre workflows reutilizáveis no GitHub Actions mostra como compartilhar essa etapa entre vários repositórios.

Suporte a Native AOT

O suporte nativo a OpenAPI no ASP.NET Core é compatível com trimming e Native AOT. Essa é uma diferença importante em relação a soluções baseadas em reflection, como o Swashbuckle.

Para criar e publicar uma aplicação com Native AOT e OpenAPI, use o template webapiaot:

dotnet new webapiaot
dotnet add package Microsoft.AspNetCore.OpenApi
dotnet publish

Antes de publicar, adicione builder.Services.AddOpenApi() e app.MapOpenApi() ao Program.cs, conforme o exemplo da documentação. Dessa forma, a aplicação continua compatível com os requisitos de Native AOT do .NET.

Configurando a versão do documento OpenAPI

No .NET 9, o Microsoft.AspNetCore.OpenApi gera documentos no formato OpenAPI 3.0. Já o .NET 10 trouxe suporte ao OpenAPI 3.1 e passou a usá-lo como padrão.

A versão fica na propriedade OpenApiVersion das opções do AddOpenApi. Por exemplo, em um projeto .NET 10 você pode fixar o formato explicitamente:

builder.Services.AddOpenApi(options =>
{
    options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_1;
});

Para o OpenAPI em tempo de build, a versão também aceita configuração via OpenApiGenerateDocumentsOptions:

<PropertyGroup>
  <OpenApiGenerateDocumentsOptions>--openapi-version OpenApi3_1</OpenApiGenerateDocumentsOptions>
</PropertyGroup>

Portanto, se o seu projeto ainda está no .NET 9, conte com o formato 3.0. Ao migrar para o .NET 10, revise ferramentas que consomem o arquivo, porque a mudança de versão altera a estrutura de alguns schemas.

Transformers: enriquecendo o documento gerado

O pacote Microsoft.AspNetCore.OpenApi também suporta transformers. Eles permitem modificar o documento antes da serialização.

Transformers servem para adicionar informações de segurança, personalizar schemas ou enriquecer a documentação de operações:

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, cancellationToken) =>
    {
        document.Info.Title = "balta.io API";
        document.Info.Version = "v1";
        return Task.CompletedTask;
    });
});

Existem três tipos de transformers: de documento, de operação e de schema. Cada um atua em um nível diferente da estrutura do documento.

Como o gerador executa o Program.cs, os transformers também rodam no OpenAPI em tempo de build. Assim, o arquivo versionado já sai com título, versão e esquemas de segurança corretos.

Conclusão

Gerar o OpenAPI em tempo de build representa uma evolução importante na gestão de contratos de API no .NET. O contrato deixa de ser um subproduto do runtime e vira um artefato versionado.

Com os pacotes Microsoft.AspNetCore.OpenApi e Microsoft.Extensions.ApiDescription.Server, você integra a geração ao pipeline sem dependências externas. Dessa forma, o contrato fica sempre atualizado para revisão, validação e geração de clientes tipados.

A compatibilidade com Native AOT, os múltiplos documentos e os transformers posicionam a solução nativa como uma alternativa robusta. Além disso, ela segue as diretrizes atuais do ecossistema .NET.

Para equipes que ainda usam Swashbuckle, a migração vale a avaliação. Isso é ainda mais verdade em projetos que querem rastrear o contrato da API no repositório e no CI.

FAQ

Por que gerar o OpenAPI no build em vez de em runtime?

A geração no build permite versionar o documento, validar o contrato no CI/CD e servir o arquivo de forma estática. Tudo isso sem depender de uma instância da aplicação rodando.

O que acontece se eu mantiver o Swashbuckle junto com o ApiDescription.Server?

Os dois pacotes conflitam e nenhum arquivo sai do build, sem mensagem de erro explícita. A solução é remover o Swashbuckle.AspNetCore.SwaggerGen.

O suporte nativo funciona com controllers, e não apenas com Minimal APIs?

Sim. O Microsoft.AspNetCore.OpenApi suporta Minimal APIs e APIs baseadas em controllers a partir do .NET 9.

O arquivo gerado pode ir para o repositório?

Sim, e essa é uma das principais vantagens. Basta apontar OpenApiDocumentsDirectory para o diretório do projeto.

Como evitar que serviços de banco de dados inicializem durante o build?

Condicione o registro desses serviços a uma verificação do assembly de entrada. Quando o nome for GetDocument.Insider, a aplicação está rodando dentro do gerador.

O suporte é compatível com Native AOT?

Sim. Diferentemente de soluções baseadas em reflection, o Microsoft.AspNetCore.OpenApi é compatível com trimming e Native AOT desde o .NET 9.

É possível gerar o documento em YAML em vez de JSON?

Em runtime, sim, mapeando o endpoint com sufixo .yaml. No entanto, a geração de YAML durante o build não era suportada no ASP.NET Core 9.0.