- Implemented comprehensive unit tests for RabbitMqTransportServer, covering constructor, disposal, connection management, event handlers, and exception handling. - Added configuration tests for RabbitMqTransportServer to validate SSL, durable queues, auto-recovery, and custom virtual host options. - Created unit tests for UdpFrameProtocol, including frame parsing and serialization, header size validation, and round-trip data preservation. - Developed tests for UdpTransportClient, focusing on connection handling, event subscriptions, and exception scenarios. - Established tests for UdpTransportServer, ensuring proper start/stop behavior, connection state management, and event handling. - Included tests for UdpTransportOptions to verify default values and modification capabilities. - Enhanced service registration tests for Udp transport services in the dependency injection container.
13 KiB
OpenAPI Aggregation
This document describes how the StellaOps Gateway aggregates OpenAPI documentation from connected microservices into a unified specification.
Overview
The Gateway automatically generates a single OpenAPI 3.1.0 document that aggregates all endpoints from connected microservices. This provides:
- Unified API documentation: All services documented in one place
- Dynamic updates: Document regenerates when services connect/disconnect
- Standard compliance: OpenAPI 3.1.0 with native JSON Schema draft 2020-12 support
- Multiple formats: Available as JSON or YAML
- Efficient caching: ETag-based caching with configurable TTL
How It Works
┌──────────────┐ HELLO ┌──────────────┐ GET /openapi.json ┌──────────────┐
│ Billing │ ──────────► │ │ ◄───────────────────── │ Client │
│ Service │ + schemas │ Gateway │ │ │
└──────────────┘ │ │ OpenAPI 3.1.0 │ │
│ │ ─────────────────────► │ │
┌──────────────┐ HELLO │ │ unified document └──────────────┘
│ Inventory │ ──────────► │ │
│ Service │ + schemas └──────────────┘
└──────────────┘
- Microservices send schemas and endpoint metadata via HELLO payload
- Gateway stores this information in routing state
- OpenAPI generator aggregates all connected services
- Document is cached and served via HTTP endpoints
Configuration
OpenApiAggregationOptions
Configure OpenAPI aggregation in your Gateway configuration:
# router.yaml or appsettings.yaml
OpenApi:
Title: "My API Gateway"
Description: "Unified API for all microservices"
Version: "2.0.0"
ServerUrl: "https://api.example.com"
CacheTtlSeconds: 60
Enabled: true
LicenseName: "AGPL-3.0-or-later"
ContactName: "API Team"
ContactEmail: "api@example.com"
TokenUrl: "/auth/token"
Configuration Reference
| Property | Type | Default | Description |
|---|---|---|---|
Title |
string |
"StellaOps Gateway API" |
API title in OpenAPI info section |
Description |
string |
"Unified API aggregating all connected microservices." |
API description |
Version |
string |
"1.0.0" |
API version number |
ServerUrl |
string |
"/" |
Base server URL |
CacheTtlSeconds |
int |
60 |
Cache time-to-live in seconds |
Enabled |
bool |
true |
Enable/disable OpenAPI aggregation |
LicenseName |
string |
"AGPL-3.0-or-later" |
License name in OpenAPI info |
ContactName |
string? |
null |
Contact name (optional) |
ContactEmail |
string? |
null |
Contact email (optional) |
TokenUrl |
string |
"/auth/token" |
OAuth2 token endpoint URL |
Disabling OpenAPI
To disable OpenAPI aggregation entirely:
OpenApi:
Enabled: false
Endpoints
Discovery Endpoint
GET /.well-known/openapi
Returns metadata about the OpenAPI document:
Response:
{
"openapi_json": "/openapi.json",
"openapi_yaml": "/openapi.yaml",
"etag": "\"5d41402abc4b2a76b9719d911017c592\"",
"generated_at": "2025-01-15T10:30:00.0000000Z"
}
OpenAPI JSON
GET /openapi.json
Returns the full OpenAPI 3.1.0 specification in JSON format.
Headers:
Cache-Control: public, max-age=60ETag: "<content-hash>"Content-Type: application/json; charset=utf-8
Conditional Request:
GET /openapi.json
If-None-Match: "5d41402abc4b2a76b9719d911017c592"
Returns 304 Not Modified if content unchanged.
OpenAPI YAML
GET /openapi.yaml
Returns the full OpenAPI 3.1.0 specification in YAML format.
Headers:
Cache-Control: public, max-age=60ETag: "<content-hash>"Content-Type: application/yaml; charset=utf-8
Security Mapping
The Gateway automatically maps claim requirements to OpenAPI security schemes.
Claim to Scope Mapping
When endpoints define RequiringClaims, these are converted to OAuth2 scopes:
// Endpoint with claim requirements
[StellaEndpoint("POST", "/invoices")]
[RequireClaim("billing:write")]
public sealed class CreateInvoiceEndpoint : IStellaEndpoint<...>
Becomes in OpenAPI:
{
"paths": {
"/invoices": {
"post": {
"security": [
{
"BearerAuth": [],
"OAuth2": ["billing:write"]
}
]
}
}
}
}
Security Schemes
The Gateway generates two security schemes:
BearerAuth
HTTP Bearer token authentication (always present):
{
"BearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "JWT Bearer token authentication"
}
}
OAuth2
Client credentials flow with collected scopes (only if endpoints have claims):
{
"OAuth2": {
"type": "oauth2",
"flows": {
"clientCredentials": {
"tokenUrl": "/auth/token",
"scopes": {
"billing:write": "Access scope: billing:write",
"billing:read": "Access scope: billing:read",
"inventory:read": "Access scope: inventory:read"
}
}
}
}
}
Scope Collection
Scopes are automatically collected from all connected services. If multiple endpoints require the same claim, it appears only once in the scopes list.
Generated Document Structure
The aggregated OpenAPI document follows this structure:
{
"openapi": "3.1.0",
"info": {
"title": "StellaOps Gateway API",
"version": "1.0.0",
"description": "Unified API aggregating all connected microservices.",
"license": {
"name": "AGPL-3.0-or-later"
},
"contact": {
"name": "API Team",
"email": "api@example.com"
}
},
"servers": [
{
"url": "/"
}
],
"paths": {
"/invoices": {
"post": {
"operationId": "billing_invoices_POST",
"tags": ["billing"],
"summary": "Create invoice",
"description": "Creates a new draft invoice",
"security": [
{
"BearerAuth": [],
"OAuth2": ["billing:write"]
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/billing_CreateInvoiceRequest"
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/billing_CreateInvoiceResponse"
}
}
}
},
"400": { "description": "Bad Request" },
"401": { "description": "Unauthorized" },
"404": { "description": "Not Found" },
"422": { "description": "Validation Error" },
"500": { "description": "Internal Server Error" }
}
}
},
"/items": {
"get": {
"operationId": "inventory_items_GET",
"tags": ["inventory"],
"summary": "List items",
"responses": {
"200": { "description": "Success" }
}
}
}
},
"components": {
"schemas": {
"billing_CreateInvoiceRequest": {
"type": "object",
"required": ["customerId", "amount"],
"properties": {
"customerId": { "type": "string" },
"amount": { "type": "number" },
"description": { "type": ["string", "null"] },
"lineItems": {
"type": "array",
"items": { "$ref": "#/components/schemas/billing_LineItem" }
}
}
},
"billing_CreateInvoiceResponse": {
"type": "object",
"required": ["invoiceId", "createdAt", "status"],
"properties": {
"invoiceId": { "type": "string" },
"createdAt": { "type": "string", "format": "date-time" },
"status": { "type": "string" }
}
},
"billing_LineItem": {
"type": "object",
"required": ["description", "amount"],
"properties": {
"description": { "type": "string" },
"amount": { "type": "number" },
"quantity": { "type": "integer", "default": 1 }
}
}
},
"securitySchemes": {
"BearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "JWT Bearer token authentication"
},
"OAuth2": {
"type": "oauth2",
"flows": {
"clientCredentials": {
"tokenUrl": "/auth/token",
"scopes": {
"billing:write": "Access scope: billing:write"
}
}
}
}
}
},
"tags": [
{
"name": "billing",
"description": "billing microservice (v1.0.0)"
},
{
"name": "inventory",
"description": "inventory microservice (v2.0.0)"
}
]
}
Schema Prefixing
Schemas are prefixed with the service name to prevent naming conflicts:
| Service | Original Type | Prefixed Schema ID |
|---|---|---|
billing |
CreateInvoiceRequest |
billing_CreateInvoiceRequest |
inventory |
GetItemResponse |
inventory_GetItemResponse |
Tag Generation
Tags are automatically generated from connected services:
- Tag name: Service name (lowercase)
- Tag description: Service description from
OpenApiInfoor auto-generated
Operation IDs
Operation IDs follow the pattern: {serviceName}_{path}_{method}
Example: billing_invoices_POST
Cache Behavior
TTL-Based Expiration
The document cache expires based on CacheTtlSeconds (default: 60 seconds):
OpenApi:
CacheTtlSeconds: 30 # More frequent regeneration
Setting CacheTtlSeconds: 0 regenerates the document on every request (not recommended for production).
Connection-Based Invalidation
The cache is automatically invalidated when:
- A new microservice connects (HELLO received)
- A microservice disconnects (connection closed)
This ensures the OpenAPI document always reflects currently connected services.
ETag Consistency
The ETag is computed from the document content hash (SHA256). This ensures:
- Same content = same ETag
- Content changes = new ETag
- Clients can use conditional requests to avoid re-downloading unchanged documents
Recommended Client Strategy
// Store ETag from previous response
let cachedETag = localStorage.getItem('openapi-etag');
const response = await fetch('/openapi.json', {
headers: cachedETag ? { 'If-None-Match': cachedETag } : {}
});
if (response.status === 304) {
// Use cached document
return getCachedDocument();
}
// Store new ETag and document
localStorage.setItem('openapi-etag', response.headers.get('ETag'));
const document = await response.json();
cacheDocument(document);
return document;
Service Registration
Microservice Options
Configure service metadata that appears in OpenAPI:
services.AddStellaMicroservice(options =>
{
options.ServiceName = "billing";
options.ServiceDescription = "Invoice and payment processing service";
options.ContactInfo = "billing-team@example.com";
});
Service OpenAPI Info
The ServiceOpenApiInfo is sent in the HELLO payload:
{
"instance": { ... },
"endpoints": [ ... ],
"schemas": { ... },
"openApiInfo": {
"title": "billing",
"description": "Invoice and payment processing service"
}
}
This description appears in the tag entry for the service.
Troubleshooting
Document Not Updating
- Check
CacheTtlSeconds- may need to wait for TTL expiration - Verify service connected successfully (check Gateway logs)
- Force refresh by restarting the Gateway
Missing Schemas
- Ensure
[ValidateSchema]attribute is applied to endpoints - Check for schema parsing errors in Gateway logs
- Verify endpoint implements
IStellaEndpoint<TRequest, TResponse>
Security Schemes Not Appearing
- OAuth2 scheme only appears if endpoints have claim requirements
- Check
RequiringClaimsis populated on endpoint descriptors - Verify claim types are being transmitted correctly
See Also
- Schema Validation - JSON Schema validation reference
- API Overview - General API conventions
- Gateway OpenAPI - Gateway OpenAPI implementation details