22. OpenAPI / Swagger

  1. OpenAPI / Swagger

About this chapter

Generate interactive API documentation automatically using OpenAPI and Swagger, keeping docs synchronized with code through annotations and XML comments.

  • OpenAPI specification: Machine-readable API contracts
  • Swagger UI: Interactive documentation and testing interface
  • Installation and configuration: Setting up Swashbuckle in ASP.NET Core
  • XML documentation: Adding summaries and descriptions to endpoints
  • Security schemes: Documenting JWT and API Key authentication
  • Response documentation: Using ProducesResponseType attributes

Learning outcomes:

  • Understand OpenAPI specification and benefits
  • Install and configure Swagger in .NET 10
  • Write XML documentation comments for endpoints
  • Document authentication schemes in Swagger
  • Configure response types for all HTTP status codes
  • Use Swagger UI to test endpoints with authentication

22.1 Understanding OpenAPI

OpenAPI is a machine-readable specification for REST APIs. Tools like Swagger UI generate interactive documentation directly from your code. Instead of maintaining separate documentation files that drift out of sync, OpenAPI keeps docs as code annotations—always current with your actual API.

Why This Matters: Clients can auto-generate SDKs, you get a free testing interface, and your API contracts are explicit and validated.

22.2 Installing and Configuring Swagger

dotnet add package Swashbuckle.AspNetCore
// Program.csbuilder.Services.AddSwaggerGen(options =>{    options.SwaggerDoc("v1", new OpenApiInfo    {        Title = "Command API",        Version = "1.0.0",        Description = "API for managing commands and platforms"    });    // Document JWT authentication    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme    {        Type = SecuritySchemeType.Http,        Scheme = "bearer",        BearerFormat = "JWT",        Description = "JWT Authorization header"    });    options.AddSecurityRequirement(new OpenApiSecurityRequirement    {        {            new OpenApiSecurityScheme            {                Reference = new OpenApiReference                {                    Type = ReferenceType.SecurityScheme,                    Id = "Bearer"                }            },            new string[] { }        }    });    // Document API Key authentication    options.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme    {        Type = SecuritySchemeType.ApiKey,        In = ParameterLocation.Header,        Name = "x-api-key",        Description = "API Key authentication"    });    options.AddSecurityRequirement(new OpenApiSecurityRequirement    {        {            new OpenApiSecurityScheme            {                Reference = new OpenApiReference                {                    Type = ReferenceType.SecurityScheme,                    Id = "ApiKey"                }            },            new string[] { }        }    });    // Include XML documentation    var xmlFile = Path.Combine(AppContext.BaseDirectory, "CommandAPI.xml");    if (File.Exists(xmlFile))        options.IncludeXmlComments(xmlFile);});var app = builder.Build();if (app.Environment.IsDevelopment()){    app.UseSwagger();    app.UseSwaggerUI(options =>    {        options.SwaggerEndpoint("/swagger/v1/swagger.json", "CommandAPI v1");    });}app.MapControllers();app.Run();

Enable XML Documentation in your .csproj:

<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <DocumentationFile>bin\$(Configuration)\$(TargetFramework)\CommandAPI.xml</DocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

22.3 Adding XML Documentation to Endpoints

Documentation comments appear in Swagger UI automatically. They guide developers on what each endpoint does and what to expect.

[Route("api/[controller]")]
[ApiController]
[Authorize(AuthenticationSchemes = ApiKeyConstants.SchemeName)]
public class PlatformsController : ControllerBase
{
    /// <summary>
    /// Get all platforms with pagination
    /// </summary>
    /// <param name="pageIndex">Page number (1-based, default: 1)</param>
    /// <param name="pageSize">Items per page (default: 10)</param>
    /// <returns>Paginated list of platforms</returns>
    /// <response code="200">Successfully retrieved platforms</response>
    /// <response code="401">Unauthorized - invalid credentials</response>
    [HttpGet]
    [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PaginatedList<PlatformReadDto>))]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<ActionResult<PaginatedList<PlatformReadDto>>> GetPlatforms(
        int pageIndex = 1,
        int pageSize = 10)
    {
        var platforms = await _repository.GetPlatformsAsync(pageIndex, pageSize);
        return Ok(_mapper.Map<PaginatedList<PlatformReadDto>>(platforms));
    }
    
    /// <summary>
    /// Get a specific platform by ID
    /// </summary>
    /// <param name="id">Platform ID</param>
    /// <returns>Platform with all details</returns>
    /// <response code="200">Platform found</response>
    /// <response code="404">Platform not found</response>
    /// <response code="401">Unauthorized</response>
    [HttpGet("{id}", Name = "GetPlatformById")]
    [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PlatformReadDto))]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<ActionResult<PlatformReadDto>> GetPlatformById(int id)
    {
        var platform = await _repository.GetPlatformByIdAsync(id);
        if (platform == null)
            return NotFound();
        return Ok(_mapper.Map<PlatformReadDto>(platform));
    }
    
    /// <summary>
    /// Create a new platform
    /// </summary>
    /// <param name="platformDto">Platform data to create</param>
    /// <returns>Created platform with generated ID</returns>
    /// <response code="201">Platform successfully created</response>
    /// <response code="400">Invalid input data</response>
    /// <response code="401">Unauthorized</response>
    [HttpPost]
    [ProducesResponseType(StatusCodes.Status201Created, Type = typeof(PlatformReadDto))]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<ActionResult<PlatformReadDto>> CreatePlatform(
        PlatformMutateDto platformDto)
    {
        var platform = _mapper.Map<Platform>(platformDto);
        platform.CreatedBy = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        await _repository.CreatePlatformAsync(platform);
        await _repository.SaveChangesAsync();
        var readDto = _mapper.Map<PlatformReadDto>(platform);
        return CreatedAtRoute(nameof(GetPlatformById), new { id = readDto.Id }, readDto);
    }
}

Key Points:

  • ProducesResponseType tells Swagger what responses are possible
  • XML comments populate descriptions in Swagger UI
  • Multiple ProducesResponseType attributes document all outcomes
  • Response codes should match actual implementation

22.4 DTOs with Example Data

Swagger’s “Try it out” feature displays examples from your DTO documentation.

public class PlatformMutateDto
{
    /// <summary>
    /// Name of the platform (e.g., Windows, Linux, Docker)
    /// </summary>
    /// <example>Docker</example>
    [Required(ErrorMessage = "Platform name is required")]
    [MaxLength(100)]
    public required string PlatformName { get; set; }
}

public class CommandMutateDto
{
    /// <summary>
    /// What this command does
    /// </summary>
    /// <example>Build Docker image</example>
    [Required]
    [MaxLength(250)]
    public required string HowTo { get; init; }
    
    /// <summary>
    /// The actual command to execute
    /// </summary>
    /// <example>docker build -t myapp .</example>
    [Required]
    public required string CommandLine { get; set; }
    
    /// <summary>
    /// Platform this command runs on
    /// </summary>
    /// <example>1</example>
    [Range(1, int.MaxValue)]
    public int PlatformId { get; set; }
}

When developers click “Try it out” in Swagger UI, these examples pre-populate the request body.

22.5 CORRECTION: Comprehensive HTTP Examples

Create a CommandAPI.http file with realistic examples. This serves as both documentation and a testing tool.

@baseUrl = https://localhost:7213
@apiKey = your-api-key-here
@jwtToken = your-jwt-token-here

### ========== PLATFORMS ==========

### Get all platforms
GET {{baseUrl}}/api/platforms?pageIndex=1&pageSize=10
x-api-key: {{apiKey}}

### Get platform by ID
GET {{baseUrl}}/api/platforms/1
x-api-key: {{apiKey}}

### Create platform
POST {{baseUrl}}/api/platforms
Content-Type: application/json
x-api-key: {{apiKey}}

{
  "platformName": "Kubernetes"
}

### Update platform
PUT {{baseUrl}}/api/platforms/1
Content-Type: application/json
x-api-key: {{apiKey}}

{
  "platformName": "Kubernetes v1.28"
}

### Delete platform
DELETE {{baseUrl}}/api/platforms/1
x-api-key: {{apiKey}}

### ========== COMMANDS ==========

### Get all commands
GET {{baseUrl}}/api/commands?pageIndex=1&pageSize=10
x-api-key: {{apiKey}}

### Get command by ID
GET {{baseUrl}}/api/commands/1
x-api-key: {{apiKey}}

### Create command
POST {{baseUrl}}/api/commands
Content-Type: application/json
x-api-key: {{apiKey}}

{
  "howTo": "Run container in production",
  "commandLine": "kubectl apply -f deployment.yaml",
  "platformId": 1
}

### Partial update command
PATCH {{baseUrl}}/api/commands/1
Content-Type: application/json-patch+json
x-api-key: {{apiKey}}

[
  {
    "op": "replace",
    "path": "/howTo",
    "value": "Run container in staging"
  }
]

### Delete command
DELETE {{baseUrl}}/api/commands/1
x-api-key: {{apiKey}}

### ========== BULK OPERATIONS ==========

### Bulk create commands (async)
POST {{baseUrl}}/api/commands/bulk
Content-Type: application/json
x-api-key: {{apiKey}}

[
  {
    "howTo": "Build image",
    "commandLine": "docker build -t app .",
    "platformId": 1
  },
  {
    "howTo": "Push to registry",
    "commandLine": "docker push myregistry/app",
    "platformId": 1
  }
]

### Check bulk job status
GET {{baseUrl}}/api/commands/checkjob/job-id-1234
x-api-key: {{apiKey}}

### Get bulk job results
GET {{baseUrl}}/api/commands/bulk/job-id-1234?pageIndex=1&pageSize=10
x-api-key: {{apiKey}}

### ========== AUTHENTICATION ==========

### Register API Key (requires JWT)
POST {{baseUrl}}/api/registrations
Content-Type: application/json
Authorization: Bearer {{jwtToken}}

{
  "description": "Production API key"
}

### ========== HEALTH ==========

### Overall health
GET {{baseUrl}}/health

### Detailed health
GET {{baseUrl}}/health/detailed

Usage: Install REST Client extension in VS Code, hover over requests, click “Send Request”. Variables like {{baseUrl}} are substituted automatically