Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- 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.
504 lines
13 KiB
Markdown
504 lines
13 KiB
Markdown
# 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 └──────────────┘
|
|
└──────────────┘
|
|
```
|
|
|
|
1. Microservices send schemas and endpoint metadata via HELLO payload
|
|
2. Gateway stores this information in routing state
|
|
3. OpenAPI generator aggregates all connected services
|
|
4. Document is cached and served via HTTP endpoints
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
### OpenApiAggregationOptions
|
|
|
|
Configure OpenAPI aggregation in your Gateway configuration:
|
|
|
|
```yaml
|
|
# 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:
|
|
|
|
```yaml
|
|
OpenApi:
|
|
Enabled: false
|
|
```
|
|
|
|
---
|
|
|
|
## Endpoints
|
|
|
|
### Discovery Endpoint
|
|
|
|
```http
|
|
GET /.well-known/openapi
|
|
```
|
|
|
|
Returns metadata about the OpenAPI document:
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"openapi_json": "/openapi.json",
|
|
"openapi_yaml": "/openapi.yaml",
|
|
"etag": "\"5d41402abc4b2a76b9719d911017c592\"",
|
|
"generated_at": "2025-01-15T10:30:00.0000000Z"
|
|
}
|
|
```
|
|
|
|
### OpenAPI JSON
|
|
|
|
```http
|
|
GET /openapi.json
|
|
```
|
|
|
|
Returns the full OpenAPI 3.1.0 specification in JSON format.
|
|
|
|
**Headers:**
|
|
- `Cache-Control: public, max-age=60`
|
|
- `ETag: "<content-hash>"`
|
|
- `Content-Type: application/json; charset=utf-8`
|
|
|
|
**Conditional Request:**
|
|
```http
|
|
GET /openapi.json
|
|
If-None-Match: "5d41402abc4b2a76b9719d911017c592"
|
|
```
|
|
|
|
Returns `304 Not Modified` if content unchanged.
|
|
|
|
### OpenAPI YAML
|
|
|
|
```http
|
|
GET /openapi.yaml
|
|
```
|
|
|
|
Returns the full OpenAPI 3.1.0 specification in YAML format.
|
|
|
|
**Headers:**
|
|
- `Cache-Control: public, max-age=60`
|
|
- `ETag: "<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:
|
|
|
|
```csharp
|
|
// Endpoint with claim requirements
|
|
[StellaEndpoint("POST", "/invoices")]
|
|
[RequireClaim("billing:write")]
|
|
public sealed class CreateInvoiceEndpoint : IStellaEndpoint<...>
|
|
```
|
|
|
|
Becomes in OpenAPI:
|
|
|
|
```json
|
|
{
|
|
"paths": {
|
|
"/invoices": {
|
|
"post": {
|
|
"security": [
|
|
{
|
|
"BearerAuth": [],
|
|
"OAuth2": ["billing:write"]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Security Schemes
|
|
|
|
The Gateway generates two security schemes:
|
|
|
|
#### BearerAuth
|
|
|
|
HTTP Bearer token authentication (always present):
|
|
|
|
```json
|
|
{
|
|
"BearerAuth": {
|
|
"type": "http",
|
|
"scheme": "bearer",
|
|
"bearerFormat": "JWT",
|
|
"description": "JWT Bearer token authentication"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### OAuth2
|
|
|
|
Client credentials flow with collected scopes (only if endpoints have claims):
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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 `OpenApiInfo` or 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):
|
|
|
|
```yaml
|
|
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:
|
|
|
|
1. A new microservice connects (HELLO received)
|
|
2. 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
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
1. Check `CacheTtlSeconds` - may need to wait for TTL expiration
|
|
2. Verify service connected successfully (check Gateway logs)
|
|
3. Force refresh by restarting the Gateway
|
|
|
|
### Missing Schemas
|
|
|
|
1. Ensure `[ValidateSchema]` attribute is applied to endpoints
|
|
2. Check for schema parsing errors in Gateway logs
|
|
3. Verify endpoint implements `IStellaEndpoint<TRequest, TResponse>`
|
|
|
|
### Security Schemes Not Appearing
|
|
|
|
1. OAuth2 scheme only appears if endpoints have claim requirements
|
|
2. Check `RequiringClaims` is populated on endpoint descriptors
|
|
3. Verify claim types are being transmitted correctly
|
|
|
|
---
|
|
|
|
## See Also
|
|
|
|
- [Schema Validation](schema-validation.md) - JSON Schema validation reference
|
|
- [API Overview](../../api/overview.md) - General API conventions
|
|
- [Gateway OpenAPI](../gateway/openapi.md) - Gateway OpenAPI implementation details
|