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
- Always provide WithName() for operation IDs
- Use meaningful names that describe the operation
- Include examples for complex requests/responses
- Document all parameters with descriptions and examples
- Specify all possible responses including errors
- Keep descriptions updated when behavior changes
- Use consistent terminology across all endpoints
Related ADRs
- 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)