Files
git.stella-ops.org/docs/modules/router/openapi-aggregation.md
master cc69d332e3
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Add unit tests for RabbitMq and Udp transport servers and clients
- 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.
2025-12-05 19:01:12 +02:00

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  └──────────────┘
└──────────────┘
  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:

# 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=60
  • ETag: "<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=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:

// 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 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):

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
// 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

  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