Documentation

adrs/017-openapi-metadata-enrichment.md

ADR-017: OpenAPI Metadata Enrichment

Status

Accepted

Context

High-quality API documentation is essential for developer experience, client generation, and API discovery. While minimal APIs can automatically generate basic OpenAPI specifications, additional metadata is needed to create comprehensive, self-documenting APIs that are easy to understand and use.

Requirements:

  • Clear, descriptive endpoint documentation
  • Meaningful operation IDs for client generation
  • Detailed parameter and response descriptions
  • Examples and schema documentation
  • Consistent documentation standards across all services

Decision

We will systematically enrich all endpoints with OpenAPI metadata using WithDisplayName() and WithDescription() methods, along with other OpenAPI extension methods, to generate comprehensive API documentation.

Pattern:

group.MapGet("/assets/{id}", GetAssetById)
    .WithName("GetAsset")                    // Operation ID
    .WithDisplayName("Get Asset by ID")      // Human-readable name
    .WithDescription("Retrieves a single asset by its unique identifier")
    .WithSummary("Get asset details")
    .Produces<Asset>(200)
    .ProducesProblem(404);

Consequences

Positive

  • Documentation Quality: Rich, self-documenting APIs
  • Client Generation: Better generated client code with Kiota
  • Developer Experience: Clear understanding of API capabilities
  • API Discovery: Easy to explore and understand endpoints
  • Consistency: Standardized documentation across services
  • Maintenance: Documentation lives with code

Negative

  • Verbosity: Additional metadata makes endpoint definitions longer
  • Maintenance Burden: Must keep documentation updated
  • Duplication: Some information may be redundant
  • Build Time: Slight increase in build time for metadata processing

Neutral

  • Code Location: Documentation inline with endpoint definition
  • Tooling Dependency: Relies on OpenAPI tooling
  • Convention Requirements: Team must follow documentation standards

Implementation Notes

Complete Endpoint Documentation

public static void MapAssetEndpoints(this IEndpointRouteBuilder endpoints)
{
    var group = endpoints.MapGroup("/api/assets")
        .WithTags("Assets")
        .WithDescription("Asset management operations");
    
    group.MapGet("/{id:int}", GetAssetById)
        // Operation identification
        .WithName("GetAsset")
        .WithDisplayName("Get Asset by ID")
        
        // Detailed descriptions
        .WithDescription("Retrieves complete asset information including " +
                        "type, location, attributes, and current status")
        .WithSummary("Get asset details")
        
        // Response documentation
        .Produces<Asset>(StatusCodes.Status200OK, "application/json")
        .ProducesProblem(StatusCodes.Status404NotFound)
        .ProducesProblem(StatusCodes.Status400BadRequest)
        
        // OpenAPI extensions
        .WithOpenApi(operation =>
        {
            operation.Parameters[0].Description = "The unique asset identifier";
            operation.Parameters[0].Example = new OpenApiString("12345");
            
            operation.Responses["200"].Description = 
                "Asset found and returned successfully";
            operation.Responses["404"].Description = 
                "Asset with specified ID not found";
                
            return operation;
        });
}

Parameter Documentation

group.MapGet("/search", SearchAssets)
    .WithName("SearchAssets")
    .WithDisplayName("Search Assets")
    .WithDescription("Search for assets using various criteria")
    .WithOpenApi(operation =>
    {
        // Document query parameters
        var queryParam = operation.Parameters.First(p => p.Name == "query");
        queryParam.Description = "Search term to match against asset name or description";
        queryParam.Example = new OpenApiString("laptop");
        queryParam.Required = false;
        
        var pageParam = operation.Parameters.First(p => p.Name == "page");
        pageParam.Description = "Page number for pagination (1-based)";
        pageParam.Example = new OpenApiInteger(1);
        
        var pageSizeParam = operation.Parameters.First(p => p.Name == "pageSize");
        pageSizeParam.Description = "Number of items per page (max 100)";
        pageSizeParam.Example = new OpenApiInteger(20);
        
        return operation;
    });

Request Body Documentation

group.MapPost("/", CreateAsset)
    .WithName("CreateAsset")
    .WithDisplayName("Create New Asset")
    .WithDescription("Creates a new asset in the system")
    .Accepts<CreateAssetRequest>("application/json")
    .Produces<Asset>(StatusCodes.Status201Created)
    .ProducesValidationProblem()
    .WithOpenApi(operation =>
    {
        operation.RequestBody.Description = 
            "Asset creation request with all required properties";
        operation.RequestBody.Required = true;
        
        // Add request examples
        operation.RequestBody.Content["application/json"].Examples = new Dictionary<string, OpenApiExample>
        {
            ["Laptop"] = new OpenApiExample
            {
                Summary = "Create a laptop asset",
                Value = new
                {
                    name = "Dell Laptop",
                    typeId = 1,
                    serialNumber = "DL123456",
                    locationId = 42
                }
            },
            ["Monitor"] = new OpenApiExample
            {
                Summary = "Create a monitor asset",
                Value = new
                {
                    name = "Samsung Monitor",
                    typeId = 2,
                    serialNumber = "SM789012",
                    locationId = 42
                }
            }
        };
        
        return operation;
    });

Response Documentation with Examples

group.MapGet("/types", GetAssetTypes)
    .WithName("GetAssetTypes")
    .WithDisplayName("Get Asset Types")
    .WithDescription("Retrieves all available asset types")
    .Produces<List<AssetType>>(200)
    .WithOpenApi(operation =>
    {
        operation.Responses["200"].Description = 
            "List of asset types returned successfully";
            
        // Add response examples
        var response = operation.Responses["200"];
        response.Content["application/json"].Examples = new Dictionary<string, OpenApiExample>
        {
            ["Default"] = new OpenApiExample
            {
                Summary = "Standard asset types",
                Value = new[]
                {
                    new { id = 1, name = "Laptop", category = "IT Equipment" },
                    new { id = 2, name = "Monitor", category = "IT Equipment" },
                    new { id = 3, name = "Desk", category = "Furniture" }
                }
            }
        };
        
        return operation;
    });

Deprecation Documentation

group.MapGet("/legacy/{code}", GetAssetByLegacyCode)
    .WithName("GetAssetByLegacyCode")
    .WithDisplayName("Get Asset by Legacy Code (Deprecated)")
    .WithDescription("DEPRECATED: Use GetAsset endpoint instead. " +
                    "This endpoint will be removed in v6.0")
    .WithOpenApi(operation =>
    {
        operation.Deprecated = true;
        operation.Extensions["x-deprecation-date"] = new OpenApiString("2025-06-01");
        operation.Extensions["x-replacement"] = new OpenApiString("/api/assets/{id}");
        return operation;
    });

Security Documentation

group.MapPost("/sensitive", ProcessSensitiveData)
    .RequireAuthorization("AdminPolicy")
    .WithName("ProcessSensitiveData")
    .WithDisplayName("Process Sensitive Data")
    .WithDescription("Processes sensitive asset data. Requires admin privileges.")
    .WithOpenApi(operation =>
    {
        operation.Security = new List<OpenApiSecurityRequirement>
        {
            new OpenApiSecurityRequirement
            {
                [new OpenApiSecurityScheme
                {
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "Bearer"
                    }
                }] = new[] { "admin", "write:assets" }
            }
        };
        
        operation.Responses["401"] = new OpenApiResponse
        {
            Description = "Authentication required"
        };
        operation.Responses["403"] = new OpenApiResponse
        {
            Description = "Insufficient permissions"
        };
        
        return operation;
    });

Metadata Standards

Operation Names (WithName)

  • PascalCase without spaces
  • Verb + Noun pattern
  • Examples: GetAsset, CreateUser, UpdateLocation

Display Names (WithDisplayName)

  • Human-readable with spaces
  • Title case
  • Examples: "Get Asset by ID", "Create New User"

Descriptions (WithDescription)

  • Complete sentences
  • Include important details
  • Mention side effects
  • Note requirements or restrictions

Summaries (WithSummary)

  • Brief, one-line description
  • Action-oriented
  • Under 120 characters

Documentation Template

// Standard endpoint documentation template
endpoint.MapMethod(path, handler)
    .WithName("{Verb}{Noun}")
    .WithDisplayName("{Human Readable Action}")
    .WithSummary("{Brief one-line summary}")
    .WithDescription("{Detailed description with context, " +
                    "requirements, and behavior}")
    .Produces<ResponseType>(200, "application/json")
    .ProducesProblem(400)
    .ProducesProblem(404)
    .WithOpenApi(ConfigureOpenApi);

Testing Documentation

[Test]
public void Endpoint_HasProperDocumentation()
{
    // Verify endpoint has required metadata
    var endpoint = GetEndpointMetadata("/api/assets/{id}");
    
    endpoint.DisplayName.Should().NotBeNullOrEmpty();
    endpoint.Description.Should().NotBeNullOrEmpty();
    endpoint.OperationId.Should().MatchRegex("^[A-Z][a-zA-Z]+$");
}

Best Practices

  1. Always provide WithName() for operation IDs
  2. Use meaningful names that describe the operation
  3. Include examples for complex requests/responses
  4. Document all parameters with descriptions and examples
  5. Specify all possible responses including errors
  6. Keep descriptions updated when behavior changes
  7. Use consistent terminology across all endpoints
  • ADR-013: Minimal APIs with Static Classes (where metadata is applied)
  • ADR-014: TypedResults for Type-Safe Responses (automatic response documentation)
  • ADR-016: Endpoint Grouping and Tagging Strategy (organization in docs)
  • ADR-021: Microsoft Kiota for API Client Generation (uses OpenAPI metadata)
  • ADR-022: Scalar API Documentation (displays enriched metadata)