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
csharp1builder.Services.AddOpenApi(options =>2{3 options.AddDocumentTransformer((document, context, ct) =>4 {5 document.Info = new OpenApiInfo6 {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 OpenApiContact12 {13 Name = "API Support",14 Email = "support@parceltracking.example.com"15 }16 };17 return Task.CompletedTask;18 });19});
Then map the endpoint:
csharp1app.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:
csharp1// Program.cs2if (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:
xml1<PropertyGroup>2 <GenerateDocumentationFile>true</GenerateDocumentationFile>3 <NoWarn>$(NoWarn);1591</NoWarn>4</PropertyGroup>
Then add comments to your controllers:
csharp1/// <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:
csharp1[ApiController]2[Route("api/v1/parcels")]3[Tags("Parcels")]4public class ParcelsController : ControllerBase5{6 // ...7}89[ApiController]10[Route("api/v1/addresses")]11[Tags("Addresses")]12public class AddressesController : ControllerBase13{14 // ...15}1617[ApiController]18[Route("api/v1/analytics")]19[Tags("Analytics")]20public class AnalyticsController : ControllerBase21{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:
csharp1/// <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:
csharp1/// <summary>2/// Request to register a new parcel.3/// </summary>4public class RegisterParcelRequest5{6 /// <summary>7 /// Weight of the parcel in kilograms.8 /// </summary>9 /// <example>2.5</example>10 public decimal WeightKg { get; set; }1112 /// <summary>13 /// ID of the sender's address.14 /// </summary>15 /// <example>1</example>16 public int SenderAddressId { get; set; }1718 /// <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:
bash1dotnet add src/ParcelTracking.Api package Asp.Versioning.Mvc2dotnet add src/ParcelTracking.Api package Asp.Versioning.Mvc.ApiExplorer
Configuring Versioning
Register the versioning services in Program.cs:
csharp1builder.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:
csharp1[ApiController]2[ApiVersion("1.0")]3[Route("api/v{version:apiVersion}/parcels")]4[Tags("Parcels")]5public class ParcelsController : ControllerBase6{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.02api-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:
csharp1[ApiController]2[ApiVersion("2.0")]3[Route("api/v{version:apiVersion}/parcels")]4[Tags("Parcels v2")]5public class ParcelsV2Controller : ControllerBase6{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 shape14 }15}
Both versions coexist. Clients on v1 continue to work while new clients adopt v2. Eventually you deprecate v1:
csharp1[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:
csharp1builder.Services.AddOpenApi("v1", options =>2{3 options.AddDocumentTransformer((document, context, ct) =>4 {5 document.Info = new OpenApiInfo6 {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:
csharp1/// <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 cannot12/// 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:
http1### Register an address (sender)2POST {{baseUrl}}/api/v1/addresses3Content-Type: application/json45{6 "street": "123 Sender St",7 "city": "Austin",8 "state": "TX",9 "postalCode": "73301",10 "country": "US"11}1213### Register an address (recipient)14POST {{baseUrl}}/api/v1/addresses15Content-Type: application/json1617{18 "street": "456 Receiver Ave",19 "city": "Seattle",20 "state": "WA",21 "postalCode": "98101",22 "country": "US"23}2425### Register a parcel26POST {{baseUrl}}/api/v1/parcels27Content-Type: application/json2829{30 "weightKg": 2.5,31 "senderAddressId": 1,32 "recipientAddressId": 233}3435### Add tracking event - picked up36POST {{baseUrl}}/api/v1/parcels/PKG-00000001/events37Content-Type: application/json3839{40 "status": "PickedUp",41 "location": "Austin Distribution Center",42 "notes": "Package collected from sender"43}4445### Add tracking event - in transit46POST {{baseUrl}}/api/v1/parcels/PKG-00000001/events47Content-Type: application/json4849{50 "status": "InTransit",51 "location": "Dallas Hub",52 "notes": "In transit to destination"53}5455### Add tracking event - out for delivery56POST {{baseUrl}}/api/v1/parcels/PKG-00000001/events57Content-Type: application/json5859{60 "status": "OutForDelivery",61 "location": "Seattle Distribution Center",62 "notes": "Out for delivery"63}6465### Confirm delivery66POST {{baseUrl}}/api/v1/parcels/PKG-00000001/events67Content-Type: application/json6869{70 "status": "Delivered",71 "location": "456 Receiver Ave, Seattle",72 "notes": "Signed by recipient"73}7475### Get parcel by tracking number76GET {{baseUrl}}/api/v1/parcels/PKG-000000017778### Get tracking history79GET {{baseUrl}}/api/v1/parcels/PKG-00000001/events8081### Check analytics82GET {{baseUrl}}/api/v1/analytics/summary8384### Health check85GET {{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
- ASP.NET Core 10 has built-in OpenAPI generation through
Microsoft.AspNetCore.OpenApi - XML documentation comments and
ProducesResponseTypedrive the generated API docs - Tags group related endpoints for a clean documentation layout
Asp.Versioning.Mvcprovides URL-based versioning with{version:apiVersion}route segments- Deprecated versions remain functional but signal clients to migrate
- An HTTP collection verifies the full API workflow end to end