OpenAPI and Documentation

Generating an accurate OpenAPI document with the built-in support in .NET 10, and turning it into a client.

Syntaxbuilder.Services.AddOpenApi(); ... app.MapOpenApi(); // GET /openapi/v1.json

OpenAPI is a file format for describing an HTTP API: every path, every method, every parameter, every response and the shape of each body, as JSON or YAML. It exists so that tools can read your API. From one document you get reference pages, a try-it-out page and a generated client in any language.

The built-in document

ASP.NET Core has generated the document itself since .NET 9, in the Microsoft.AspNetCore.OpenApi package. builder.Services.AddOpenApi() registers the generator and app.MapOpenApi() serves the result at /openapi/{documentName}.json, where the name defaults to v1. In .NET 10 the document is OpenAPI 3.1; set OpenApiOptions.OpenApiVersion if a consumer needs 3.0. Adding Microsoft.Extensions.ApiDescription.Server writes the file at build time instead, so it can be committed and diffed.

There is no browsable UI in the box. Swagger UI and Scalar are separate packages that read the document you already serve.

Making it accurate

The generator reports the status codes it can see. Return Results<Ok<Book>, NotFound> and both appear. Return a bare IResult and it cannot tell, so declare them with .Produces<T>() and .ProducesProblem(), or [ProducesResponseType] on a controller action. New in .NET 10: set <GenerateDocumentationFile>true</GenerateDocumentationFile> and a source generator folds your XML doc comments into the document at compile time.

Example

Example · csharp
using Microsoft.AspNetCore.Http.HttpResults;

var builder = WebApplication.CreateBuilder(args);

// Needs: dotnet add package Microsoft.AspNetCore.OpenApi
// Registers the generator. The document is named "v1" unless you say otherwise.
builder.Services.AddOpenApi();

var app = builder.Build();

// Serves the document at /openapi/{documentName}.json
app.MapOpenApi();

// The declared return type names both outcomes, so the document lists a 200
// with a Book schema and a 404 with no body. A bare IResult would list neither.
app.MapGet("/books/{id:int}", Results<Ok<Book>, NotFound> (int id) =>
        id == 1
            ? TypedResults.Ok(new Book(1, "Dune"))
            : TypedResults.NotFound())
   .WithSummary("Get one book by id")
   .WithDescription("Returns 404 when no book has that id.");

app.Run();

public record Book(int Id, string Title);

When to use it

  • Handing a frontend team a document they can generate a typed client from instead of a wiki page.
  • Catching a response shape change in code review by diffing the OpenAPI file generated at build time.
  • Publishing reference docs and a try-it-out page by pointing Scalar or Swagger UI at /openapi/v1.json.
  • Splitting one app's endpoints into a public document for partners and an internal one for your own tools.

More examples

Declaring responses when the handler returns IResult

Produces<T> adds one response with a status code and a schema; ProducesValidationProblem adds the 400 with a validation body. Reach for these when a handler has several exits and a union return type would get unwieldy, or when you are describing an existing endpoint you do not want to rewrite.

Example · csharp
// Here the return type is just IResult, so the generator cannot infer
// anything. Each response is declared instead.
app.MapPost("/books", (CreateBookRequest request, IBookStore store) =>
{
    if (string.IsNullOrWhiteSpace(request.Title))
    {
        return Results.ValidationProblem(new Dictionary<string, string[]>
        {
            ["title"] = ["Title is required."]
        });
    }

    Book created = store.Add(request.Title);
    return Results.Created($"/books/{created.Id}", created);
})
.Produces<Book>(StatusCodes.Status201Created)
.ProducesValidationProblem();

XML doc comments as the source of the descriptions

This is the .NET 10 addition. The comments that your editor already shows on hover become the summaries and descriptions in the document, so there is one text to keep current rather than two. The handler is a named static method rather than a lambda, because a lambda has nowhere to put the comments.

Example · csharp
using Microsoft.AspNetCore.Http.HttpResults;

// Requires <GenerateDocumentationFile>true</GenerateDocumentationFile>
// in the .csproj. Nothing else: a source generator does the rest.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();

var app = builder.Build();
app.MapOpenApi();
app.MapGet("/books/{id:int}", BookEndpoints.GetById);
app.Run();

public static class BookEndpoints
{
    /// <summary>Retrieves one book.</summary>
    /// <param name="id">The id of the book to retrieve.</param>
    /// <response code="200">The book was found.</response>
    /// <response code="404">No book has that id.</response>
    public static Results<Ok<Book>, NotFound> GetById(int id) =>
        id == 1 ? TypedResults.Ok(new Book(1, "Dune")) : TypedResults.NotFound();
}

public record Book(int Id, string Title);

Generating a C# client from the document

The generated code gives the consuming project real types and method names instead of hand-written URL strings. Regenerate it whenever the document changes and the compiler points at every call that no longer fits. Use this across a codebase where one service calls another and both are yours.

Example · bash
# Install the generator once.
dotnet tool install --global Microsoft.OpenApi.Kiota

# Point it at the document your app serves, or at the committed file.
kiota generate \
  --openapi http://localhost:5000/openapi/v1.json \
  --language csharp \
  --output ./Catalog.Client \
  --class-name CatalogClient \
  --namespace-name Catalog.Client

# Then add the packages kiota lists for C#:
kiota info --language csharp

Discussion

  • Be the first to comment on this lesson.