OpenAPI and Swagger Generation in ASP.NET Core
OpenAPI (formerly Swagger) specifications describe your API's endpoints, request/response shapes, authentication schemes, and more in a machine-readable format. ASP.NET Core 9+ includes built-in OpenAPI document generation via Microsoft.AspNetCore.OpenApi, replacing the long-standing reliance on Swashbuckle.
Built-in OpenAPI Support (.NET 9+)
ASP.NET Core 9 introduced native OpenAPI document generation:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
app.MapOpenApi();
app.MapGet("/api/products", () => new[] { new Product(1, "Widget", 9.99m) })
.WithName("GetProducts")
.WithDescription("Returns all products");
app.Run();
The OpenAPI document is served at /openapi/v1.json by default. To add a UI for browsing the API, use Scalar or Swagger UI as static middleware.
Adding Swagger UI
While the built-in support generates the document, you'll likely want a UI. Swagger UI remains the most popular option:
dotnet add package Swashbuckle.AspNetCore.SwaggerUI
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/openapi/v1.json", "My API v1");
});
Describing Endpoints
Use extension methods to enrich endpoint metadata:
app.MapPost("/api/products", async (CreateProductRequest request, AppDbContext db) =>
{
var product = new Product { Name = request.Name, Price = request.Price };
db.Products.Add(product);
await db.SaveChangesAsync();
return TypedResults.Created($"/api/products/{product.Id}", product);
})
.WithName("CreateProduct")
.WithDescription("Creates a new product")
.WithTags("Products")
.Produces<Product>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.ProducesProblem(StatusCodes.Status409Conflict);
The Produces and ProducesProblem calls tell OpenAPI what response types to expect for each status code.
Controller-Based Documentation
With controllers, use attributes and XML comments:
[ApiController]
[Route("api/products")]
[Produces("application/json")]
[Tags("Products")]
public class ProductsController : ControllerBase
{
/// <summary>
/// Gets a product by ID.
/// </summary>
/// <param name="id">The product identifier.</param>
/// <returns>The requested product.</returns>
/// <response code="200">Returns the product.</response>
/// <response code="404">Product not found.</response>
[HttpGet("{id}")]
[ProducesResponseType<Product>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetById(int id)
{
// ...
}
}
Enable XML comments in your .csproj:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
Customising the OpenAPI Document
Transform the generated document to add global metadata, security definitions, or modify schemas:
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
document.Info = new OpenApiInfo
{
Title = "Product Catalogue API",
Version = "v1",
Description = "API for managing the product catalogue.",
Contact = new OpenApiContact
{
Name = "API Support",
Email = "[email protected]"
}
};
return Task.CompletedTask;
});
});
Adding Security Definitions
Document authentication requirements in the OpenAPI specification:
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes = new Dictionary<string, OpenApiSecurityScheme>
{
["Bearer"] = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
Description = "Enter your JWT token"
}
};
return Task.CompletedTask;
});
options.AddOperationTransformer((operation, context, cancellationToken) =>
{
if (context.Description.ActionDescriptor.EndpointMetadata
.OfType<AuthorizeAttribute>().Any())
{
operation.Security = new List<OpenApiSecurityRequirement>
{
new()
{
[new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
}] = Array.Empty<string>()
}
};
}
return Task.CompletedTask;
});
});
Schema Customisation
Transform schemas to add examples or modify property metadata:
options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
if (context.JsonTypeInfo.Type == typeof(Product))
{
schema.Example = new OpenApiObject
{
["id"] = new OpenApiInteger(1),
["name"] = new OpenApiString("Widget"),
["price"] = new OpenApiDouble(9.99)
};
}
return Task.CompletedTask;
});
Multiple API Versions
Generate separate documents for different API versions:
builder.Services.AddOpenApi("v1", options =>
{
options.AddDocumentTransformer((doc, _, _) =>
{
doc.Info.Title = "My API";
doc.Info.Version = "1.0";
return Task.CompletedTask;
});
});
builder.Services.AddOpenApi("v2", options =>
{
options.AddDocumentTransformer((doc, _, _) =>
{
doc.Info.Title = "My API";
doc.Info.Version = "2.0";
return Task.CompletedTask;
});
});
app.MapOpenApi("/openapi/{documentName}.json");
Key Takeaways
- ASP.NET Core 9+ includes built-in OpenAPI generation — Swashbuckle is no longer required for document generation.
- Use
Produces,WithDescription, andWithTagsto enrich your endpoint metadata. - Document transformers let you customise the specification at the document, operation, and schema levels.
- Always document security schemes so API consumers know how to authenticate.
- Generate separate documents for each API version to keep specifications clean.
A good OpenAPI specification is living documentation that stays in sync with your code. It powers client generation, testing tools, and API portals — making it worth the investment to get right.