15 minlesson

OpenAPI Documentation & API Versioning

OpenAPI Documentation & API Versioning

In this presentation, you will configure comprehensive OpenAPI documentation for the parcel tracking API and add URL-based API versioning so the contract can evolve safely.

OpenAPI in ASP.NET Core 10

ASP.NET Core 10 includes built-in OpenAPI document generation. The Microsoft.AspNetCore.OpenApi package replaces the older Swashbuckle approach with a leaner, first-party integration.

Registering OpenAPI

csharp
1builder.Services.AddOpenApi(options =>
2{
3 options.AddDocumentTransformer((document, context, ct) =>
4 {
5 document.Info = new OpenApiInfo
6 {
7 Title = "Parcel Tracking API",
8 Version = "v1",
9 Description = "A production-grade REST API for parcel registration, " +
10 "tracking, and delivery management.",
11 Contact = new OpenApiContact
12 {
13 Name = "API Support",
14 Email = "support@parceltracking.example.com"
15 }
16 };
17 return Task.CompletedTask;
18 });
19});

Then map the endpoint:

csharp
1app.MapOpenApi();

This generates an OpenAPI 3.0 document at /openapi/v1.json that describes every mapped endpoint.

Adding Swagger UI

To provide an interactive documentation page, add the Scalar or Swagger UI package. Scalar is the modern choice:

csharp
1// Program.cs
2if (app.Environment.IsDevelopment())
3{
4 app.MapScalarApiReference();
5}

This serves an interactive API explorer at /scalar/v1 where developers can browse endpoints and send test requests.

XML Documentation Comments

OpenAPI descriptions come from XML doc comments on your controller actions. Enable XML generation in the project file:

xml
1<PropertyGroup>
2 <GenerateDocumentationFile>true</GenerateDocumentationFile>
3 <NoWarn>$(NoWarn);1591</NoWarn>
4</PropertyGroup>

Then add comments to your controllers:

csharp
1/// <summary>
2/// Registers a new parcel in the tracking system.
3/// </summary>
4/// <param name="request">The parcel registration details.</param>
5/// <returns>The newly created parcel with its tracking number.</returns>
6/// <response code="201">Parcel registered successfully.</response>
7/// <response code="400">Validation failed for the request.</response>
8[HttpPost]
9[ProducesResponseType(typeof(ParcelResponse), StatusCodes.Status201Created)]
10[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
11public async Task<IActionResult> RegisterParcel(RegisterParcelRequest request)
12{
13 // ...
14}

The ProducesResponseType attributes tell OpenAPI which status codes and response shapes each endpoint can return. Combined with XML comments, the generated documentation is complete and accurate.

Grouping Endpoints with Tags

Tags organize endpoints into logical groups in the documentation UI. Apply tags at the controller level:

csharp
1[ApiController]
2[Route("api/v1/parcels")]
3[Tags("Parcels")]
4public class ParcelsController : ControllerBase
5{
6 // ...
7}
8
9[ApiController]
10[Route("api/v1/addresses")]
11[Tags("Addresses")]
12public class AddressesController : ControllerBase
13{
14 // ...
15}
16
17[ApiController]
18[Route("api/v1/analytics")]
19[Tags("Analytics")]
20public class AnalyticsController : ControllerBase
21{
22 // ...
23}

Endpoints are then grouped under "Parcels", "Addresses", and "Analytics" sections in the documentation.

Request and Response Examples

You can enrich the documentation with concrete examples using the OpenApiExample attribute or document transformers:

csharp
1/// <summary>
2/// Registers a new parcel in the tracking system.
3/// </summary>
4[HttpPost]
5public async Task<IActionResult> RegisterParcel(
6 [FromBody] RegisterParcelRequest request)
7{
8 // ...
9}

For the request DTO, use XML comments on each property:

csharp
1/// <summary>
2/// Request to register a new parcel.
3/// </summary>
4public class RegisterParcelRequest
5{
6 /// <summary>
7 /// Weight of the parcel in kilograms.
8 /// </summary>
9 /// <example>2.5</example>
10 public decimal WeightKg { get; set; }
11
12 /// <summary>
13 /// ID of the sender's address.
14 /// </summary>
15 /// <example>1</example>
16 public int SenderAddressId { get; set; }
17
18 /// <summary>
19 /// ID of the recipient's address.
20 /// </summary>
21 /// <example>2</example>
22 public int RecipientAddressId { get; set; }
23}

The <example> XML tag populates the example values shown in Swagger UI, so consumers see realistic data instead of default zeros and empty strings.

API Versioning with Asp.Versioning

Install the versioning package:

bash
1dotnet add src/ParcelTracking.Api package Asp.Versioning.Mvc
2dotnet add src/ParcelTracking.Api package Asp.Versioning.Mvc.ApiExplorer

Configuring Versioning

Register the versioning services in Program.cs:

csharp
1builder.Services.AddApiVersioning(options =>
2{
3 options.DefaultApiVersion = new ApiVersion(1, 0);
4 options.AssumeDefaultVersionWhenUnspecified = true;
5 options.ReportApiVersions = true;
6 options.ApiVersionReader = new UrlSegmentApiVersionReader();
7})
8.AddApiExplorer(options =>
9{
10 options.GroupNameFormat = "'v'VVV";
11 options.SubstituteApiVersionInUrl = true;
12});

This configures URL-based versioning where the version appears as a route segment.

Applying Versions to Controllers

Mark each controller with the version it belongs to and use {version:apiVersion} in the route template:

csharp
1[ApiController]
2[ApiVersion("1.0")]
3[Route("api/v{version:apiVersion}/parcels")]
4[Tags("Parcels")]
5public class ParcelsController : ControllerBase
6{
7 /// <summary>
8 /// Retrieves a parcel by its tracking number.
9 /// </summary>
10 [HttpGet("{trackingNumber}")]
11 public async Task<IActionResult> GetByTrackingNumber(string trackingNumber)
12 {
13 // ...
14 }
15}

The SubstituteApiVersionInUrl option means the OpenAPI document shows clean URLs like /api/v1/parcels/{trackingNumber} instead of the raw template.

The ReportApiVersions Header

When ReportApiVersions is true, every response includes headers telling the client which versions are available:

1api-supported-versions: 1.0
2api-deprecated-versions: (none yet)

This lets clients discover available versions without consulting documentation.

Evolving to v2

When you need to introduce a breaking change, create a new controller version:

csharp
1[ApiController]
2[ApiVersion("2.0")]
3[Route("api/v{version:apiVersion}/parcels")]
4[Tags("Parcels v2")]
5public class ParcelsV2Controller : ControllerBase
6{
7 /// <summary>
8 /// Retrieves a parcel by tracking number with extended details.
9 /// </summary>
10 [HttpGet("{trackingNumber}")]
11 public async Task<IActionResult> GetByTrackingNumber(string trackingNumber)
12 {
13 // Return a new response shape
14 }
15}

Both versions coexist. Clients on v1 continue to work while new clients adopt v2. Eventually you deprecate v1:

csharp
1[ApiVersion("1.0", Deprecated = true)]

Deprecated versions still function but signal to clients that they should migrate.

Generating Separate OpenAPI Documents Per Version

Configure OpenAPI to generate one document per API version:

csharp
1builder.Services.AddOpenApi("v1", options =>
2{
3 options.AddDocumentTransformer((document, context, ct) =>
4 {
5 document.Info = new OpenApiInfo
6 {
7 Title = "Parcel Tracking API",
8 Version = "v1"
9 };
10 return Task.CompletedTask;
11 });
12});

This produces /openapi/v1.json for v1 endpoints only. When v2 arrives, you add a second registration with "v2".

Complete XML Comment Coverage

Every public endpoint should have XML documentation covering:

  • <summary> -- What the endpoint does.
  • <param> -- What each parameter means.
  • <returns> -- What the response contains.
  • <response> -- Each possible status code and when it occurs.
  • <remarks> -- Additional usage notes if needed.

Example of thorough documentation:

csharp
1/// <summary>
2/// Adds a tracking event to an existing parcel.
3/// </summary>
4/// <param name="trackingNumber">The parcel's unique tracking number.</param>
5/// <param name="request">The tracking event details.</param>
6/// <returns>The created tracking event.</returns>
7/// <response code="201">Tracking event added successfully.</response>
8/// <response code="404">Parcel with the given tracking number was not found.</response>
9/// <response code="409">The event violates the parcel's status lifecycle rules.</response>
10/// <remarks>
11/// Events must follow the status lifecycle. For example, a parcel cannot
12/// go from Delivered back to InTransit.
13/// </remarks>
14[HttpPost("{trackingNumber}/events")]
15[ProducesResponseType(typeof(TrackingEventResponse), StatusCodes.Status201Created)]
16[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
17[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
18public async Task<IActionResult> AddTrackingEvent(
19 string trackingNumber,
20 [FromBody] AddTrackingEventRequest request)
21{
22 // ...
23}

Postman / HTTP Collection

A complete HTTP collection exercises the full API workflow. Create a .http file that tests the endpoints in sequence:

http
1### Register an address (sender)
2POST {{baseUrl}}/api/v1/addresses
3Content-Type: application/json
4
5{
6 "street": "123 Sender St",
7 "city": "Austin",
8 "state": "TX",
9 "postalCode": "73301",
10 "country": "US"
11}
12
13### Register an address (recipient)
14POST {{baseUrl}}/api/v1/addresses
15Content-Type: application/json
16
17{
18 "street": "456 Receiver Ave",
19 "city": "Seattle",
20 "state": "WA",
21 "postalCode": "98101",
22 "country": "US"
23}
24
25### Register a parcel
26POST {{baseUrl}}/api/v1/parcels
27Content-Type: application/json
28
29{
30 "weightKg": 2.5,
31 "senderAddressId": 1,
32 "recipientAddressId": 2
33}
34
35### Add tracking event - picked up
36POST {{baseUrl}}/api/v1/parcels/PKG-00000001/events
37Content-Type: application/json
38
39{
40 "status": "PickedUp",
41 "location": "Austin Distribution Center",
42 "notes": "Package collected from sender"
43}
44
45### Add tracking event - in transit
46POST {{baseUrl}}/api/v1/parcels/PKG-00000001/events
47Content-Type: application/json
48
49{
50 "status": "InTransit",
51 "location": "Dallas Hub",
52 "notes": "In transit to destination"
53}
54
55### Add tracking event - out for delivery
56POST {{baseUrl}}/api/v1/parcels/PKG-00000001/events
57Content-Type: application/json
58
59{
60 "status": "OutForDelivery",
61 "location": "Seattle Distribution Center",
62 "notes": "Out for delivery"
63}
64
65### Confirm delivery
66POST {{baseUrl}}/api/v1/parcels/PKG-00000001/events
67Content-Type: application/json
68
69{
70 "status": "Delivered",
71 "location": "456 Receiver Ave, Seattle",
72 "notes": "Signed by recipient"
73}
74
75### Get parcel by tracking number
76GET {{baseUrl}}/api/v1/parcels/PKG-00000001
77
78### Get tracking history
79GET {{baseUrl}}/api/v1/parcels/PKG-00000001/events
80
81### Check analytics
82GET {{baseUrl}}/api/v1/analytics/summary
83
84### Health check
85GET {{baseUrl}}/health

This collection walks through the complete lifecycle: register addresses, create a parcel, move it through each status, confirm delivery, then verify the data through retrieval and analytics endpoints.

Key Takeaways

  1. ASP.NET Core 10 has built-in OpenAPI generation through Microsoft.AspNetCore.OpenApi
  2. XML documentation comments and ProducesResponseType drive the generated API docs
  3. Tags group related endpoints for a clean documentation layout
  4. Asp.Versioning.Mvc provides URL-based versioning with {version:apiVersion} route segments
  5. Deprecated versions remain functional but signal clients to migrate
  6. An HTTP collection verifies the full API workflow end to end