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:

Program.cs
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:

terminal
dotnet add package Swashbuckle.AspNetCore.SwaggerUI
Example.cs
app.UseSwaggerUI(options =>
{
    options.SwaggerEndpoint("/openapi/v1.json", "My API v1");
});

Describing Endpoints

Use extension methods to enrich endpoint metadata:

Example.cs
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:

ProductsController.cs
[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:

MyApp.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:

Example.cs
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:

Example.cs
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:

Example.cs
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:

Example.cs
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

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.