By the end of this session, you will be able to:
[ProducesResponseType] decorators| Time | Segment | Type | Duration |
|---|---|---|---|
| 0:00 | Why documentation matters | Discussion | 10 min |
| 0:10 | OpenAPI spec overview | Theory | 15 min |
| 0:25 | Swashbuckle setup | Demo | 25 min |
| 0:50 | XML comments & decorators | Demo | 20 min |
| 1:10 | Break | — | 10 min |
| 1:20 | Versioned Swagger UI | Demo | 20 min |
| 1:40 | Lab — document the Task API | Lab | 35 min |
| 2:15 | Generating a typed client | Demo | 15 min |
Every caller of your API needs to know — without reading source code:
/swagger/v1/swagger.json and Swagger UI reads it to build the visual interfaceAn OpenAPI document is a JSON file with three key sections:
info — title, version, and description of the APIpaths — every endpoint, its HTTP methods, parameters, request bodies, and all possible responsescomponents — reusable schema definitions (your DTOs) referenced throughout pathsSwashbuckle generates this entire document automatically from your controllers. You only annotate your actions — the serialization is handled for you.
{
"openapi": "3.0.1",
"info": {
"title": "Task Manager API",
"version": "v1"
},
"paths": {
"/api/tasks/{id}": {
"get": {
"summary": "Returns a single task by ID.",
"parameters": [
{ "name": "id", "in": "path", "required": true }
],
"responses": {
"200": { "description": "Task found." },
"404": { "description": "Task not found." }
}
}
}
}
}
One NuGet package brings in everything needed:
dotnet add package Swashbuckle.AspNetCore
This installs three sub-packages:
Swashbuckle.AspNetCore.Swagger — serializes the spec to JSONSwashbuckle.AspNetCore.SwaggerGen — reflects over controllers to build the specSwashbuckle.AspNetCore.SwaggerUI — serves the interactive web interfaceEnable XML documentation output so comments appear in the spec:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
using System.Reflection;
using Microsoft.OpenApi.Models;
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Task Manager API",
Version = "v1",
Description = "RESTful API for managing tasks"
});
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
IncludeXmlComments wires the XML documentation file into Swashbuckle so your /// <summary> and /// <response> tags appear in Swagger UI.
Two middleware calls activate Swagger — both gated to development only:
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint(
"/swagger/v1/swagger.json",
"Task Manager API v1");
});
}
What this gives you in development:
/swagger/v1/swagger.json — the raw OpenAPI document/swagger — the Swagger UI web pageIsDevelopment() guard ensures it only runs locally. Leaking it in production exposes your full API surface to anyone who finds the URL.
Three XML tags matter for Swagger:
<summary> — one-line description shown as the endpoint title in Swagger UI<param name="x"> — description of a parameter, shown in the Parameters panel<response code="N"> — human-readable description of a specific HTTP response code///. Visual Studio and Rider auto-generate the tag scaffolding when you type /// directly above a method signature.
/// <summary>Returns a single task by ID.</summary>
/// <param name="id">The task identifier.</param>
/// <response code="200">Task found and returned.</response>
/// <response code="404">No task with this ID exists.</response>
[HttpGet("{id}")]
public IActionResult GetById(int id)
=> Ok(_taskService.GetById(id));
Without the XML comments, Swagger UI shows only the method and path. With them, it shows a description, parameter labels, and a response code table with meanings.
[ProducesResponseType] tells Swashbuckle which DTO type is returned for each response code. Without it, Swagger only knows the code exists — not the shape of the body.
StatusCodes constant for the HTTP codeProblemDetails for error responses — it matches the RFC 7807 shape your API already returns from Session 05/// <summary>Returns a single task by ID.</summary>
/// <param name="id">The task identifier.</param>
/// <response code="200">Task found and returned.</response>
/// <response code="404">No task with this ID exists.</response>
[HttpGet("{id}")]
[ProducesResponseType(typeof(TaskItemDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public IActionResult GetById(int id)
=> Ok(_taskService.GetById(id));
TaskItemDto schema in the 200 panel and the ProblemDetails schema in the 404 panel — consumers see the exact shape of both outcomes.
/// <summary>Creates a new task.</summary>
/// <param name="dto">The task data.</param>
/// <response code="201">Task created successfully.</response>
/// <response code="400">Validation failed — see errors in the response body.</response>
/// <response code="409">A task with this title already exists.</response>
[HttpPost]
[ProducesResponseType(typeof(TaskItemDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
public IActionResult Create([FromBody] CreateTaskDto dto)
{
var created = _taskService.Create(dto);
return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
}
Use ValidationProblemDetails for 400 responses — it has the same shape as the validation errors ASP.NET Core returns automatically, so the schema in Swagger UI will be accurate.
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Task Manager API", Version = "v1" });
c.SwaggerDoc("v2", new OpenApiInfo { Title = "Task Manager API", Version = "v2" });
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
c.SwaggerEndpoint("/swagger/v2/swagger.json", "v2");
});
Each SwaggerDoc call creates a separate tab in Swagger UI. The matching SwaggerEndpoint calls tell the UI where to load each spec. Both the name passed to SwaggerDoc and the JSON path must use the same version string.
Swashbuckle decides which spec document an action belongs to by reading its ApiExplorerSettings.GroupName. The value must match the document name passed to SwaggerDoc.
GroupName appear in all specsGroupName = "v2" restricts the action to the v2 tab only[ApiVersion], and GroupName all use the same version identifier[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/tasks")]
[ApiExplorerSettings(GroupName = "v2")]
public class TasksV2Controller : ControllerBase
{
// Actions here appear only in the v2 tab
}
AddApiVersioning setup and route templates from Session 06 are the prerequisite. GroupName is a Swagger-only hint — it does not change how the router dispatches requests.
NSwag reads your OpenAPI spec and generates a fully typed TypeScript client. Every endpoint becomes a method; every DTO becomes an interface.
dotnet tool install --global NSwag.ConsoleCore
nswag openapi2tsclient \
/input:swagger.json \
/output:src/api/taskApiClient.ts
Download swagger.json from /swagger/v1/swagger.json while the API is running locally, then run NSwag against that file.
import { TasksClient } from '../api/taskApiClient';
const client = new TasksClient("https://localhost:5001");
// Autocomplete works for all parameters and return types
const result = await client.getAll(
1, // page
20, // pageSize
"meeting" // search
);
console.log(result.data); // TaskItemDto[]
console.log(result.totalCount); // number
Add full Swagger documentation to the Task API built in previous sessions. Every action must have XML comments and response type decorators.
Swashbuckle.AspNetCore and add GenerateDocumentationFile to the .csproj
AddEndpointsApiExplorer, AddSwaggerGen with OpenApiInfo and IncludeXmlComments in Program.cs
UseSwagger and UseSwaggerUI inside the IsDevelopment() guard
/// <summary>, /// <param>, and /// <response> XML comments to every controller action
[ProducesResponseType] for every possible response code, linking each to the correct DTO or ProblemDetails type
/swagger and verify each endpoint shows its description, parameter labels, and response schemas
GET /api/tasks?page=1&pageSize=5 and confirm the paginated response matches the displayed schema
SwaggerDoc call creates a named spec; ApiExplorerSettings routes actions into the correct oneSession 09 — Entity Framework Core Intro
DbContext, define entity models, and issue your first migration to generate a database schema from C# codeDbSet queries — replacing the in-memory collections entirelyAdd complete Swagger/OpenAPI documentation to the Task Manager API.
What to build:
AddSwaggerGen and the Swagger middleware in Program.cs/// <summary>, /// <param>, and /// <response> XML doc comments to every controller action[ProducesResponseType] decorators for all possible response codes, linking each to the correct DTO or ProblemDetails typeAcceptance criteria:
/swagger in development and lists all Task API endpoints/swagger/v1/swagger.json is valid JSON with no missing schema referencesAddSecurityDefinition and AddSecurityRequirement so the "Authorize" button appears and bearer tokens can be passed to protected endpoints directly from the UI. You will use this in Session 14 when JWT auth is implemented.