O .NET 9 mudou a forma como geramos documentação OpenAPI: o Swashbuckle — pacote de terceiros usado por praticamente toda API .NET há anos — deixou de ser a opção padrão. O framework agora tem suporte nativo a OpenAPI via Microsoft.AspNetCore.OpenApi. Este artigo cobre a migração e as funcionalidades avançadas da nova abordagem.
O que mudou no .NET 9
No .NET 8 e anteriores, o template padrão gerava:
dotnet add package Swashbuckle.AspNetCore
No .NET 9, o template usa:
dotnet add package Microsoft.AspNetCore.OpenApi # Já incluído no SDK
A diferença não é apenas de pacote — é arquitetural. O Swashbuckle gera o documento OpenAPI em runtime via reflection. O suporte nativo usa source generation e transformers em tempo de build, resultando em startup mais rápido e documentação mais precisa.
Setup básico no .NET 9
// Program.cs — .NET 9
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi(options =>
{
options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_0;
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
document.Info = new OpenApiInfo
{
Title = "Neryx Orders API",
Version = "v1",
Description = "API de pedidos da plataforma Neryx",
Contact = new OpenApiContact
{
Name = "Equipe Neryx",
Email = "api@neryx.com.br",
Url = new Uri("https://neryx.com.br")
},
License = new OpenApiLicense
{
Name = "Proprietário",
Url = new Uri("https://neryx.com.br/termos")
}
};
return Task.CompletedTask;
});
});
var app = builder.Build();
// Endpoint que serve o documento JSON
app.MapOpenApi(); // GET /openapi/v1.json
// Scalar UI (alternativa moderna ao Swagger UI no .NET 9)
app.MapScalarApiReference(); // GET /scalar/v1
// Swagger UI tradicional (ainda funciona com pacote adicional)
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/openapi/v1.json", "Neryx Orders API v1");
});
Scalar: o novo Swagger UI recomendado
dotnet add package Scalar.AspNetCore
// Scalar com configurações avançadas
app.MapScalarApiReference(options =>
{
options.WithTitle("Neryx API")
.WithTheme(ScalarTheme.DeepSpace) // Tema escuro elegante
.WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient)
.WithPreferredScheme("Bearer") // JWT como padrão
.WithHttpBearerAuthentication(bearer =>
{
bearer.Token = "seu-jwt-para-testes"; // Token pré-preenchido (só em dev)
});
});
Anotando endpoints com metadados
// Minimal APIs — metadados inline
app.MapGet("/api/products/{id:guid}", async (Guid id, IProductService service) =>
{
var product = await service.GetByIdAsync(id);
return product is null ? Results.NotFound() : Results.Ok(product);
})
.WithName("GetProductById")
.WithSummary("Busca produto por ID")
.WithDescription("Retorna os detalhes completos de um produto ativo pelo seu identificador único.")
.WithTags("Products")
.Produces<ProductDto>(StatusCodes.Status200OK)
.Produces<ProblemDetails>(StatusCodes.Status404NotFound)
.RequireAuthorization();
// Controllers — via atributos XML + OpenAPI
/// <summary>
/// Cria um novo pedido para o cliente autenticado.
/// </summary>
/// <param name="request">Dados do pedido a ser criado.</param>
/// <returns>O pedido criado com ID e status inicial.</returns>
/// <response code="201">Pedido criado com sucesso.</response>
/// <response code="400">Dados de entrada inválidos.</response>
/// <response code="422">Regra de negócio violada.</response>
[HttpPost]
[ProducesResponseType(typeof(OrderDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> Create([FromBody] CreateOrderRequest request)
{
// ...
}
Versionamento de API com documentação separada
dotnet add package Asp.Versioning.Http
dotnet add package Asp.Versioning.Mvc.ApiExplorer
// Program.cs
builder.Services
.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true; // Adiciona headers api-supported-versions
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(), // /api/v1/products
new HeaderApiVersionReader("X-Version"), // Header: X-Version: 1
new QueryStringApiVersionReader("v")); // ?v=1
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV"; // v1, v2
options.SubstituteApiVersionInUrl = true;
});
// Documento OpenAPI separado por versão
builder.Services.AddOpenApi("v1", options =>
{
options.AddDocumentTransformer<V1DocumentTransformer>();
});
builder.Services.AddOpenApi("v2", options =>
{
options.AddDocumentTransformer<V2DocumentTransformer>();
});
// Endpoints para cada versão
app.MapOpenApi("/openapi/{documentName}.json"); // /openapi/v1.json, /openapi/v2.json
app.MapScalarApiReference(options =>
{
options.WithEndpointPrefix("/scalar/{documentName}")
.AddDocuments("v1", "v2");
});
// Controllers versionados
[ApiController]
[ApiVersion(1)]
[ApiVersion(2)]
[Route("api/v{version:apiVersion}/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet("{id:guid}")]
[MapToApiVersion(1)]
public async Task<IActionResult> GetByIdV1(Guid id)
{
// Resposta da v1 (simplificada)
}
[HttpGet("{id:guid}")]
[MapToApiVersion(2)]
public async Task<IActionResult> GetByIdV2(Guid id)
{
// Resposta da v2 (com campos adicionais)
}
}
Configurando autenticação JWT no Swagger UI
// Document transformer para adicionar esquema de segurança JWT
public class JwtSecuritySchemeTransformer : IOpenApiDocumentTransformer
{
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;
public JwtSecuritySchemeTransformer(
IAuthenticationSchemeProvider authenticationSchemeProvider)
=> _authenticationSchemeProvider = authenticationSchemeProvider;
public async Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken cancellationToken)
{
var authenticationSchemes = await _authenticationSchemeProvider.GetAllSchemesAsync();
if (authenticationSchemes.Any(s => s.Name == JwtBearerDefaults.AuthenticationScheme))
{
var requirements = new Dictionary<string, OpenApiSecurityScheme>
{
["Bearer"] = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
Description = "Insira o token JWT. Exemplo: **eyJhbGci...**"
}
};
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes = requirements;
// Aplica o esquema de segurança a todos os endpoints
foreach (var operation in document.Paths.Values.SelectMany(p => p.Operations))
{
operation.Value.Security.Add(new OpenApiSecurityRequirement
{
[new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Id = "Bearer",
Type = ReferenceType.SecurityScheme
}
}] = Array.Empty<string>()
});
}
}
}
}
// Registrar
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer<JwtSecuritySchemeTransformer>();
});
Operation transformer: enriquecer operações automaticamente
// Adiciona automaticamente resposta 401/403 em todos endpoints com [Authorize]
public class AuthorizationOperationTransformer : IOpenApiOperationTransformer
{
public Task TransformAsync(
OpenApiOperation operation,
OpenApiOperationTransformerContext context,
CancellationToken cancellationToken)
{
var hasAuthorize = context.Description.ActionDescriptor.EndpointMetadata
.OfType<AuthorizeAttribute>()
.Any();
if (!hasAuthorize) return Task.CompletedTask;
operation.Responses.TryAdd("401", new OpenApiResponse
{
Description = "Não autenticado — token JWT ausente ou inválido"
});
operation.Responses.TryAdd("403", new OpenApiResponse
{
Description = "Sem permissão para este recurso"
});
return Task.CompletedTask;
}
}
// Schema transformer: adiciona exemplos a tipos comuns
public class ExampleSchemaTransformer : IOpenApiSchemaTransformer
{
public Task TransformAsync(
OpenApiSchema schema,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken)
{
// Adiciona exemplos para tipos comuns
if (context.JsonTypeInfo.Type == typeof(Guid))
{
schema.Example = new OpenApiString("3fa85f64-5717-4562-b3fc-2c963f66afa6");
}
else if (context.JsonTypeInfo.Type == typeof(decimal))
{
schema.Example = new OpenApiDouble(99.90);
}
else if (context.JsonTypeInfo.Type == typeof(DateTime))
{
schema.Example = new OpenApiString("2026-05-07T10:00:00Z");
}
return Task.CompletedTask;
}
}
// Registrar transformers
builder.Services.AddOpenApi(options =>
{
options.AddOperationTransformer<AuthorizationOperationTransformer>();
options.AddSchemaTransformer<ExampleSchemaTransformer>();
});
Expondo o documento apenas em não-produção
// Boas práticas de segurança: nunca exponha Swagger em produção
if (app.Environment.IsDevelopment() || app.Environment.IsStaging())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
// Alternativa: proteger com autenticação básica em staging
app.MapOpenApi().RequireAuthorization("ApiDocs");
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ApiDocs", policy =>
policy.RequireRole("Developer", "Admin"));
});
Gerando o documento OpenAPI em tempo de build (CI/CD)
# Gerar o arquivo openapi.json durante o build para versionamento
dotnet add package Microsoft.Extensions.ApiDescription.Server
# No .csproj:
# <PropertyGroup>
# <OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>
# <OpenApiDocumentsDirectory>./docs</OpenApiDocumentsDirectory>
# </PropertyGroup>
# Após dotnet build, o arquivo docs/v1.json é gerado
# Use no pipeline para:
# 1. Diff de breaking changes entre versões
# 2. Geração de clientes SDK automaticamente
# 3. Validação de contrato antes do merge
# .github/workflows/api-docs.yml
- name: Gerar documentação OpenAPI
run: dotnet build --configuration Release
- name: Verificar breaking changes na API
run: |
# Comparar o novo openapi.json com o da branch main
npx @openapitools/openapi-diff \
./docs/v1-main.json \
./docs/v1.json \
--fail-on-incompatible
- name: Publicar docs no GitHub Pages
uses: peaceiris/actions-gh-pages@v3
if: github.ref == 'refs/heads/main'
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs
Migração do Swashbuckle para o suporte nativo
| Swashbuckle | .NET 9 nativo |
|---|---|
AddSwaggerGen() |
AddOpenApi() |
UseSwagger() |
MapOpenApi() |
UseSwaggerUI() |
MapScalarApiReference() |
IDocumentFilter |
IOpenApiDocumentTransformer |
IOperationFilter |
IOpenApiOperationTransformer |
ISchemaFilter |
IOpenApiSchemaTransformer |
SwaggerGen.IncludeXmlComments() |
Automático via XML comments |
Uma API bem documentada é cartão de visita técnico. Se você precisa de uma documentação OpenAPI completa e integrada ao seu pipeline de CI/CD, fale com a Neryx.