docs consolidation and others

This commit is contained in:
master
2026-01-06 19:02:21 +02:00
parent d7bdca6d97
commit 4789027317
849 changed files with 16551 additions and 66770 deletions

View File

@@ -91,14 +91,14 @@ StellaOps.Router.slnx
| [aspnet-endpoint-bridge.md](aspnet-endpoint-bridge.md) | Using ASP.NET endpoint registration as Router endpoint registration |
| [messaging-valkey-transport.md](messaging-valkey-transport.md) | Messaging transport over Valkey |
### Implementation Guides (docs/router/)
### Implementation Guides (docs/modules/router/guides/)
| Document | Purpose |
|----------|---------|
| [README.md](../../router/README.md) | Quick start and feature overview |
| [ARCHITECTURE.md](../../router/ARCHITECTURE.md) | Detailed architecture walkthrough |
| [GETTING_STARTED.md](../../router/GETTING_STARTED.md) | Step-by-step setup guide |
| [rate-limiting.md](../../router/rate-limiting.md) | Rate limiting configuration guide |
| [transports/](../../router/transports/) | Transport plugin documentation |
| [README.md](guides/README.md) | Quick start and feature overview |
| [ARCHITECTURE.md](guides/ARCHITECTURE.md) | Detailed architecture walkthrough |
| [GETTING_STARTED.md](guides/GETTING_STARTED.md) | Step-by-step setup guide |
| [rate-limiting-config.md](guides/rate-limiting-config.md) | Rate limiting configuration guide |
| [transports.md](guides/transports.md) | Transport plugin documentation |
## Quick Start

View File

@@ -0,0 +1,370 @@
# Getting Started with StellaOps Router
This guide walks you through building your first microservice with the StellaOps Router framework.
## Prerequisites
- .NET 10 SDK
- Docker (optional, for containerized deployment)
## Step 1: Create the Microservice Project
```bash
mkdir MyService
cd MyService
dotnet new console -n MyService
cd MyService
```
Add the required packages:
```bash
dotnet add package StellaOps.Microservice
dotnet add package StellaOps.Router.Transport.InMemory
```
## Step 2: Define Your Data Models
Create `Models/OrderModels.cs`:
```csharp
namespace MyService.Models;
public sealed class CreateOrderRequest
{
public required string CustomerId { get; init; }
public required decimal Amount { get; init; }
public string? Description { get; init; }
}
public sealed class CreateOrderResponse
{
public required string OrderId { get; init; }
public DateTimeOffset CreatedAt { get; init; }
public string Status { get; init; } = "created";
}
public sealed class Order
{
public required string OrderId { get; init; }
public required string CustomerId { get; init; }
public required decimal Amount { get; init; }
public string? Description { get; init; }
public DateTimeOffset CreatedAt { get; init; }
}
```
## Step 3: Create Endpoints
Create `Endpoints/CreateOrderEndpoint.cs`:
```csharp
using MyService.Models;
using StellaOps.Microservice;
namespace MyService.Endpoints;
/// <summary>
/// Creates a new order.
/// </summary>
[StellaEndpoint("POST", "/orders", TimeoutSeconds = 30)]
public sealed class CreateOrderEndpoint : IStellaEndpoint<CreateOrderRequest, CreateOrderResponse>
{
private readonly ILogger<CreateOrderEndpoint> _logger;
public CreateOrderEndpoint(ILogger<CreateOrderEndpoint> logger)
{
_logger = logger;
}
public Task<CreateOrderResponse> HandleAsync(
CreateOrderRequest request,
CancellationToken cancellationToken)
{
var orderId = $"ORD-{Guid.NewGuid():N}"[..16];
_logger.LogInformation(
"Created order {OrderId} for customer {CustomerId}, amount: {Amount}",
orderId, request.CustomerId, request.Amount);
return Task.FromResult(new CreateOrderResponse
{
OrderId = orderId,
CreatedAt = DateTimeOffset.UtcNow
});
}
}
```
Create `Endpoints/GetOrderEndpoint.cs`:
```csharp
using MyService.Models;
using StellaOps.Microservice;
namespace MyService.Endpoints;
/// <summary>
/// Retrieves an order by ID.
/// </summary>
[StellaEndpoint("GET", "/orders/{id}", TimeoutSeconds = 10)]
public sealed class GetOrderEndpoint : IRawStellaEndpoint
{
private readonly ILogger<GetOrderEndpoint> _logger;
public GetOrderEndpoint(ILogger<GetOrderEndpoint> logger)
{
_logger = logger;
}
public Task<RawResponse> HandleAsync(
RawRequestContext context,
CancellationToken cancellationToken)
{
var orderId = context.PathParameters["id"];
_logger.LogInformation("Fetching order {OrderId}", orderId);
// In a real app, you'd look up the order from a database
var order = new Order
{
OrderId = orderId,
CustomerId = "CUST-001",
Amount = 99.99m,
Description = "Sample order",
CreatedAt = DateTimeOffset.UtcNow.AddHours(-1)
};
var json = System.Text.Json.JsonSerializer.Serialize(order);
var body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(json));
var headers = new HeaderCollection();
headers.Set("Content-Type", "application/json");
return Task.FromResult(new RawResponse
{
StatusCode = 200,
Headers = headers,
Body = body
});
}
}
```
## Step 4: Configure the Service
Update `Program.cs`:
```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MyService.Endpoints;
using StellaOps.Microservice;
using StellaOps.Router.Common.Enums;
using StellaOps.Router.Transport.InMemory;
var builder = Host.CreateApplicationBuilder(args);
// Configure logging
builder.Logging.AddConsole();
builder.Logging.SetMinimumLevel(LogLevel.Debug);
// Configure the microservice
builder.Services.AddStellaMicroservice(options =>
{
options.ServiceName = "order-service";
options.Version = "1.0.0";
options.Region = "local";
options.InstanceId = $"order-{Environment.MachineName}";
// Connect to gateway (use InMemory for development)
options.Routers =
[
new RouterEndpointConfig
{
Host = "localhost",
Port = 5100,
TransportType = TransportType.InMemory
}
];
});
// Register transport
builder.Services.AddInMemoryTransport();
// Register endpoint handlers
builder.Services.AddScoped<CreateOrderEndpoint>();
builder.Services.AddScoped<GetOrderEndpoint>();
var host = builder.Build();
Console.WriteLine("Order Service starting...");
Console.WriteLine("Endpoints:");
Console.WriteLine(" POST /orders");
Console.WriteLine(" GET /orders/{id}");
await host.RunAsync();
```
## Step 5: Create the Gateway
Create a separate project for the gateway:
```bash
cd ..
dotnet new web -n MyGateway
cd MyGateway
dotnet add package StellaOps.Router.Gateway
dotnet add package StellaOps.Router.Transport.InMemory
```
Update `Program.cs`:
```csharp
using StellaOps.Router.Gateway;
using StellaOps.Router.Gateway.Authorization;
using StellaOps.Router.Gateway.DependencyInjection;
using StellaOps.Router.Transport.InMemory;
var builder = WebApplication.CreateBuilder(args);
// Add gateway services
builder.Services.AddRouterGateway(builder.Configuration);
// Add InMemory transport for development
builder.Services.AddInMemoryTransport();
// No-op authorization for demo (use real auth in production)
builder.Services.AddNoOpAuthorityIntegration();
builder.Services.AddAuthentication();
// OpenAPI
builder.Services.AddEndpointsApiExplorer();
var app = builder.Build();
// Middleware pipeline
app.UseAuthentication();
app.UseClaimsAuthorization();
// OpenAPI endpoint (aggregates from all microservices)
app.MapRouterOpenApi();
// Health check
app.MapGet("/health", () => Results.Ok(new { status = "healthy" }));
// Router gateway handles all other routes
app.UseRouterGateway();
Console.WriteLine("Gateway starting on http://localhost:5000");
app.Run("http://localhost:5000");
```
## Step 6: Run the Services
Open two terminals:
**Terminal 1 - Gateway:**
```bash
cd MyGateway
dotnet run
```
**Terminal 2 - Microservice:**
```bash
cd MyService
dotnet run
```
## Step 7: Test the API
Create an order:
```bash
curl -X POST http://localhost:5000/orders \
-H "Content-Type: application/json" \
-d '{"customerId": "CUST-001", "amount": 99.99}'
```
Response:
```json
{
"orderId": "ORD-abc123def456",
"createdAt": "2024-01-15T10:30:00Z",
"status": "created"
}
```
Get an order:
```bash
curl http://localhost:5000/orders/ORD-abc123def456
```
## Step 8: Add Validation (Optional)
Add JSON Schema validation to your endpoints:
```csharp
using StellaOps.Microservice;
using StellaOps.Microservice.Validation;
[StellaEndpoint("POST", "/orders", TimeoutSeconds = 30)]
[ValidateSchema] // Enables JSON Schema validation
public sealed class CreateOrderEndpoint : IStellaEndpoint<CreateOrderRequest, CreateOrderResponse>
{
// ...
}
```
The source generator will create a JSON Schema based on your request type's properties and validate incoming requests automatically.
## Step 9: Add Streaming Support (Optional)
For large file uploads or real-time data:
```csharp
[StellaEndpoint("POST", "/orders/{id}/documents", SupportsStreaming = true)]
public sealed class UploadDocumentEndpoint : IRawStellaEndpoint
{
public async Task<RawResponse> HandleAsync(
RawRequestContext context,
CancellationToken cancellationToken)
{
var orderId = context.PathParameters["id"];
var contentType = context.Headers["Content-Type"] ?? "application/octet-stream";
// Stream the body directly without buffering
await using var bodyStream = context.Body;
var totalBytes = 0L;
var buffer = new byte[8192];
int bytesRead;
while ((bytesRead = await bodyStream.ReadAsync(buffer, cancellationToken)) > 0)
{
totalBytes += bytesRead;
// Process chunk...
}
var headers = new HeaderCollection();
headers.Set("Content-Type", "application/json");
var response = $$"""{"orderId":"{{orderId}}","bytesReceived":{{totalBytes}}}""";
return new RawResponse
{
StatusCode = 200,
Headers = headers,
Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(response))
};
}
}
```
## Next Steps
- **Production Deployment**: Switch from InMemory to TCP or TLS transport
- **Authentication**: Integrate with the Authority module for OAuth/OIDC
- **Rate Limiting**: Configure rate limits in router.yaml
- **Observability**: Add OpenTelemetry tracing
- **Testing**: Write integration tests using the Router.Testing library
See the [examples](../examples/) directory for complete working examples.

View File

@@ -0,0 +1,122 @@
# Router Rate Limiting
Router rate limiting is a **gateway-owned** control plane feature implemented in `StellaOps.Router.Gateway`. It enforces limits centrally so microservices do not implement ad-hoc HTTP throttling.
## Behavior
When a request is denied the Router returns:
- `429 Too Many Requests`
- `Retry-After: <seconds>`
- `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (Unix seconds)
- JSON body:
```json
{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded. Try again in 12 seconds.",
"retryAfter": 12,
"limit": 100,
"current": 101,
"window": 60,
"scope": "environment"
}
```
## Model
Two scopes exist:
- **Instance (`for_instance`)**: in-memory sliding window; protects a single Router process.
- **Environment (`for_environment`)**: Valkey-backed fixed window; protects the whole environment across Router instances.
Environment checks are gated by an **activation threshold** (`process_back_pressure_when_more_than_per_5min`) to avoid unnecessary Valkey calls at low traffic.
## Configuration
Configuration is under the `rate_limiting` root.
### Minimal (instance only)
```yaml
rate_limiting:
process_back_pressure_when_more_than_per_5min: 5000
for_instance:
rules:
- per_seconds: 60
max_requests: 600
```
### Environment (Valkey)
```yaml
rate_limiting:
process_back_pressure_when_more_than_per_5min: 0 # always check environment
for_environment:
valkey_connection: "valkey.stellaops.local:6379"
valkey_bucket: "stella-router-rate-limit"
circuit_breaker:
failure_threshold: 5
timeout_seconds: 30
half_open_timeout: 10
rules:
- per_seconds: 60
max_requests: 600
```
### Rule stacking (AND logic)
Multiple rules on the same target are evaluated with **AND** semantics:
```yaml
rate_limiting:
for_environment:
rules:
- per_seconds: 1
max_requests: 10
- per_seconds: 3600
max_requests: 3000
```
If any rule is exceeded the request is denied. The Router returns the **most restrictive** `Retry-After` among violated rules.
### Microservice overrides
Overrides are **replacement**, not merge:
```yaml
rate_limiting:
for_environment:
rules:
- per_seconds: 60
max_requests: 600
microservices:
scanner:
rules:
- per_seconds: 10
max_requests: 50
```
### Route overrides
Route-level configuration is under:
`rate_limiting.for_environment.microservices.<microservice>.routes.<route_name>`
See `docs/modules/router/guides/rate-limiting-routes.md` for match types and specificity rules.
## Notes
- If `rules` is present, it takes precedence over legacy single-window keys (`per_seconds`, `max_requests`, `allow_*`).
- For allowed requests, headers represent the **smallest window** rule for deterministic, low-cardinality output (not a full multi-rule snapshot).
- If Valkey is unavailable, environment limiting is **fail-open** (instance limits still apply).
## Testing
- Unit tests: `dotnet test StellaOps.Router.slnx -c Release`
- Valkey integration tests (Docker required): `STELLAOPS_INTEGRATION_TESTS=true dotnet test StellaOps.Router.slnx -c Release --filter FullyQualifiedName~ValkeyRateLimitStoreIntegrationTests`
- k6 load tests: `tests/load/router-rate-limiting-load-test.js` (see `tests/load/README.md`)

View File

@@ -0,0 +1,90 @@
# Per-Route Rate Limiting (Router)
This document describes **per-route** rate limiting configuration for the Router gateway (`StellaOps.Router.Gateway`).
## Overview
Per-route rate limiting lets you apply different limits to specific HTTP paths **within the same microservice**.
Configuration is nested as:
`rate_limiting.for_environment.microservices.<microservice>.routes.<route_name>`
## Configuration
### Example (rules + routes)
```yaml
rate_limiting:
for_environment:
valkey_connection: "valkey.stellaops.local:6379"
valkey_bucket: "stella-router-rate-limit"
# Default environment rules (used when no microservice override exists)
rules:
- per_seconds: 60
max_requests: 600
microservices:
scanner:
# Default rules for the microservice (used when no route override exists)
rules:
- per_seconds: 60
max_requests: 600
routes:
scan_submit:
pattern: "/api/scans"
match_type: exact
rules:
- per_seconds: 10
max_requests: 50
scan_status:
pattern: "/api/scans/*"
match_type: prefix
rules:
- per_seconds: 1
max_requests: 100
scan_by_id:
pattern: "^/api/scans/[a-f0-9-]+$"
match_type: regex
rules:
- per_seconds: 1
max_requests: 50
```
### Match types
`match_type` supports:
- `exact`: exact path match (case-insensitive), ignoring a trailing `/`.
- `prefix`: literal prefix match; patterns commonly end with `*` (e.g. `/api/scans/*`).
- `regex`: regular expression (compiled at startup; invalid regex fails fast).
### Specificity rules
When multiple routes match a path, the most specific match wins:
1. `exact`
2. `prefix` (longest prefix wins)
3. `regex` (longest pattern wins)
## Inheritance (resolution)
Rate limiting rules resolve with **replacement** semantics:
- `routes.<route_name>.rules` replaces the microservice rules.
- `microservices.<name>.rules` replaces the environment rules.
- If a level provides no rules, the next-less-specific level applies.
## Notes
- Per-route rate limiting applies at the **environment** scope (Valkey-backed).
- The Router returns `429 Too Many Requests` and a `Retry-After` header when a limit is exceeded.
## See also
- `docs/modules/router/guides/rate-limiting-config.md` (full configuration guide)
- `docs/modules/router/rate-limiting.md` (module dossier)

View File

@@ -32,8 +32,8 @@ This page is the module-level dossier for centralized rate limiting in the Route
- If a microservice must keep internal protection (e.g., expensive job submission), ensure it is semantically distinct from HTTP admission control and does not produce conflicting client UX.
## Documents
- Configuration guide: `docs/router/rate-limiting.md`
- Per-route guide: `docs/router/rate-limiting-routes.md`
- Ops runbook: `docs/operations/router-rate-limiting.md`
- Configuration guide: `./guides/rate-limiting-config.md`
- Per-route guide: `./guides/rate-limiting-routes.md`
- Ops runbook: `../../operations/router-rate-limiting.md`
- Testing: `tests/StellaOps.Router.Gateway.Tests/` and `tests/load/router-rate-limiting-load-test.js`

View File

@@ -0,0 +1,37 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples.Gateway", "src\Examples.Gateway\Examples.Gateway.csproj", "{A1B2C3D4-E5F6-1234-5678-9ABCDEF01234}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples.Billing.Microservice", "src\Examples.Billing.Microservice\Examples.Billing.Microservice.csproj", "{B2C3D4E5-F6A1-2345-6789-ABCDEF012345}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples.Inventory.Microservice", "src\Examples.Inventory.Microservice\Examples.Inventory.Microservice.csproj", "{C3D4E5F6-A1B2-3456-789A-BCDEF0123456}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples.Integration.Tests", "tests\Examples.Integration.Tests\Examples.Integration.Tests.csproj", "{D4E5F6A1-B2C3-4567-89AB-CDEF01234567}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A1B2C3D4-E5F6-1234-5678-9ABCDEF01234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-1234-5678-9ABCDEF01234}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-1234-5678-9ABCDEF01234}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-1234-5678-9ABCDEF01234}.Release|Any CPU.Build.0 = Release|Any CPU
{B2C3D4E5-F6A1-2345-6789-ABCDEF012345}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B2C3D4E5-F6A1-2345-6789-ABCDEF012345}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2C3D4E5-F6A1-2345-6789-ABCDEF012345}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2C3D4E5-F6A1-2345-6789-ABCDEF012345}.Release|Any CPU.Build.0 = Release|Any CPU
{C3D4E5F6-A1B2-3456-789A-BCDEF0123456}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C3D4E5F6-A1B2-3456-789A-BCDEF0123456}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3D4E5F6-A1B2-3456-789A-BCDEF0123456}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3D4E5F6-A1B2-3456-789A-BCDEF0123456}.Release|Any CPU.Build.0 = Release|Any CPU
{D4E5F6A1-B2C3-4567-89AB-CDEF01234567}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D4E5F6A1-B2C3-4567-89AB-CDEF01234567}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D4E5F6A1-B2C3-4567-89AB-CDEF01234567}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D4E5F6A1-B2C3-4567-89AB-CDEF01234567}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,297 @@
# StellaOps Router Example
This example demonstrates the StellaOps Router, Gateway, and Microservice SDK working together.
## Overview
The example includes:
- **Examples.Gateway** - HTTP gateway that routes requests to microservices
- **Examples.Billing.Microservice** - Sample billing service with typed and streaming endpoints
- **Examples.Inventory.Microservice** - Sample inventory service demonstrating multi-service routing
- **Examples.Integration.Tests** - End-to-end integration tests
## Prerequisites
- .NET 10 SDK
- Docker and Docker Compose (for containerized deployment)
## Project Structure
```
examples/router/
├── Examples.Router.sln
├── docker-compose.yaml
├── README.md
├── src/
│ ├── Examples.Gateway/
│ │ ├── Program.cs
│ │ ├── router.yaml
│ │ └── appsettings.json
│ ├── Examples.Billing.Microservice/
│ │ ├── Program.cs
│ │ ├── microservice.yaml
│ │ └── Endpoints/
│ │ ├── CreateInvoiceEndpoint.cs
│ │ ├── GetInvoiceEndpoint.cs
│ │ └── UploadAttachmentEndpoint.cs
│ └── Examples.Inventory.Microservice/
│ ├── Program.cs
│ └── Endpoints/
│ ├── ListItemsEndpoint.cs
│ └── GetItemEndpoint.cs
└── tests/
└── Examples.Integration.Tests/
```
## Running Locally
### Build the Solution
```bash
cd examples/router
dotnet build Examples.Router.sln
```
### Run with Docker Compose
```bash
docker-compose up --build
```
This starts:
- Gateway on port 8080 (HTTP) and 5100 (TCP transport)
- Billing microservice
- Inventory microservice
- RabbitMQ (optional, for message-based transport)
### Run Without Docker
Start each service in separate terminals:
```bash
# Terminal 1: Gateway
cd src/Examples.Gateway
dotnet run
# Terminal 2: Billing Microservice
cd src/Examples.Billing.Microservice
dotnet run
# Terminal 3: Inventory Microservice
cd src/Examples.Inventory.Microservice
dotnet run
```
## Example API Calls
### Billing Service
Create an invoice:
```bash
curl -X POST http://localhost:8080/invoices \
-H "Content-Type: application/json" \
-d '{"customerId": "CUST-001", "amount": 99.99, "description": "Service fee"}'
```
Get an invoice:
```bash
curl http://localhost:8080/invoices/INV-12345
```
Upload an attachment (streaming):
```bash
curl -X POST http://localhost:8080/invoices/INV-12345/attachments \
-H "Content-Type: application/octet-stream" \
--data-binary @document.pdf
```
### Inventory Service
List items:
```bash
curl "http://localhost:8080/items?page=1&pageSize=20"
```
List items by category:
```bash
curl "http://localhost:8080/items?category=widgets"
```
Get a specific item:
```bash
curl http://localhost:8080/items/SKU-001
```
## Adding New Endpoints
### 1. Create the Endpoint Class
```csharp
using StellaOps.Microservice;
[StellaEndpoint("POST", "/orders", TimeoutSeconds = 30)]
public sealed class CreateOrderEndpoint : IStellaEndpoint<CreateOrderRequest, CreateOrderResponse>
{
public Task<CreateOrderResponse> HandleAsync(
CreateOrderRequest request,
CancellationToken cancellationToken)
{
// Implementation
return Task.FromResult(new CreateOrderResponse { OrderId = "ORD-123" });
}
}
```
### 2. Register in Program.cs
```csharp
builder.Services.AddScoped<CreateOrderEndpoint>();
```
### 3. Update router.yaml (if needed)
Add routing rules for the new endpoint path.
## Streaming Endpoints
For endpoints that handle large payloads (file uploads, etc.), implement `IRawStellaEndpoint`:
```csharp
[StellaEndpoint("POST", "/files/{id}", SupportsStreaming = true)]
public sealed class UploadFileEndpoint : IRawStellaEndpoint
{
public async Task<RawResponse> HandleAsync(
RawRequestContext context,
CancellationToken cancellationToken)
{
var id = context.PathParameters["id"];
// Stream body directly without buffering
await using var stream = context.Body;
// Process stream...
return RawResponse.Ok("{}");
}
}
```
## Cancellation Behavior
All endpoints receive a `CancellationToken` that is triggered when:
1. The client disconnects
2. The request timeout is exceeded
3. The gateway shuts down
Always respect the cancellation token in long-running operations:
```csharp
public async Task<Response> HandleAsync(Request request, CancellationToken ct)
{
// Check cancellation periodically
ct.ThrowIfCancellationRequested();
// Or pass to async operations
await SomeLongOperation(ct);
}
```
## Payload Limits
Default limits are configured in `router.yaml`:
```yaml
payloadLimits:
maxRequestBodySizeBytes: 10485760 # 10 MB
maxChunkSizeBytes: 65536 # 64 KB
```
For streaming endpoints, the body is not buffered so these limits apply per-chunk.
## Running Tests
```bash
cd tests/Examples.Integration.Tests
dotnet test
```
The integration tests verify:
- End-to-end request routing
- Multi-service registration
- Streaming uploads
- Request cancellation
- Payload limit enforcement
## Configuration
### Gateway (router.yaml)
```yaml
# Microservice routing rules
services:
billing:
routes:
- path: /invoices
methods: [GET, POST]
- path: /invoices/{id}
methods: [GET, PUT, DELETE]
- path: /invoices/{id}/attachments
methods: [POST]
inventory:
routes:
- path: /items
methods: [GET]
- path: /items/{sku}
methods: [GET]
```
### Microservice (microservice.yaml)
```yaml
service:
name: billing
version: 1.0.0
region: demo
endpoints:
- path: /invoices
method: POST
timeoutSeconds: 30
- path: /invoices/{id}
method: GET
timeoutSeconds: 10
routers:
- host: localhost
port: 5100
transportType: InMemory
```
## Troubleshooting
### Microservice not registering
Check that:
1. Gateway is running and healthy
2. Router host/port in microservice.yaml matches gateway
3. Network connectivity between services
### Request timeouts
Increase the timeout in the endpoint attribute:
```csharp
[StellaEndpoint("POST", "/long-operation", TimeoutSeconds = 120)]
```
### Streaming not working
Ensure the endpoint:
1. Is marked with `SupportsStreaming = true`
2. Implements `IRawStellaEndpoint`
3. Does not buffer the entire body before processing
## License
AGPL-3.0-or-later

View File

@@ -0,0 +1,75 @@
version: '3.8'
services:
gateway:
build:
context: .
dockerfile: src/Examples.Gateway/Dockerfile
ports:
- "8080:8080" # HTTP ingress
- "5100:5100" # TCP transport
- "5101:5101" # TLS transport
environment:
- ASPNETCORE_URLS=http://+:8080
- GatewayNode__Region=demo
- GatewayNode__NodeId=gw-01
- GatewayNode__ListenPort=5100
volumes:
- ./src/Examples.Gateway/router.yaml:/app/router.yaml:ro
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 10s
timeout: 5s
retries: 3
billing:
build:
context: .
dockerfile: src/Examples.Billing.Microservice/Dockerfile
environment:
- Stella__ServiceName=billing
- Stella__Region=demo
- Stella__Routers__0__Host=gateway
- Stella__Routers__0__Port=5100
- Stella__Routers__0__TransportType=InMemory
volumes:
- ./src/Examples.Billing.Microservice/microservice.yaml:/app/microservice.yaml:ro
depends_on:
gateway:
condition: service_healthy
inventory:
build:
context: .
dockerfile: src/Examples.Inventory.Microservice/Dockerfile
environment:
- Stella__ServiceName=inventory
- Stella__Region=demo
- Stella__Routers__0__Host=gateway
- Stella__Routers__0__Port=5100
- Stella__Routers__0__TransportType=InMemory
depends_on:
gateway:
condition: service_healthy
# Optional: RabbitMQ for message-based transport
rabbitmq:
image: rabbitmq:3-management-alpine
ports:
- "5672:5672" # AMQP
- "15672:15672" # Management UI
environment:
- RABBITMQ_DEFAULT_USER=stellaops
- RABBITMQ_DEFAULT_PASS=stellaops
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "check_running"]
interval: 10s
timeout: 5s
retries: 3
networks:
default:
name: stellaops-router-example
volumes:
rabbitmq-data:

View File

@@ -0,0 +1,70 @@
using Microsoft.Extensions.Logging;
using StellaOps.Microservice;
namespace Examples.Billing.Microservice.Endpoints;
/// <summary>
/// Request model for creating an invoice.
/// </summary>
public sealed record CreateInvoiceRequest
{
public required string CustomerId { get; init; }
public required decimal Amount { get; init; }
public string? Description { get; init; }
public List<LineItem> LineItems { get; init; } = [];
}
/// <summary>
/// Line item for an invoice.
/// </summary>
public sealed record LineItem
{
public required string Description { get; init; }
public required decimal Amount { get; init; }
public int Quantity { get; init; } = 1;
}
/// <summary>
/// Response model after creating an invoice.
/// </summary>
public sealed record CreateInvoiceResponse
{
public required string InvoiceId { get; init; }
public required DateTime CreatedAt { get; init; }
public required string Status { get; init; }
}
/// <summary>
/// Endpoint for creating a new invoice.
/// Demonstrates a typed endpoint with JSON request/response.
/// </summary>
[StellaEndpoint("POST", "/invoices", TimeoutSeconds = 30)]
public sealed class CreateInvoiceEndpoint : IStellaEndpoint<CreateInvoiceRequest, CreateInvoiceResponse>
{
private readonly ILogger<CreateInvoiceEndpoint> _logger;
public CreateInvoiceEndpoint(ILogger<CreateInvoiceEndpoint> logger)
{
_logger = logger;
}
public Task<CreateInvoiceResponse> HandleAsync(
CreateInvoiceRequest request,
CancellationToken cancellationToken)
{
_logger.LogInformation(
"Creating invoice for customer {CustomerId} with amount {Amount}",
request.CustomerId,
request.Amount);
// Simulate invoice creation
var invoiceId = $"INV-{Guid.NewGuid():N}".ToUpperInvariant()[..16];
return Task.FromResult(new CreateInvoiceResponse
{
InvoiceId = invoiceId,
CreatedAt = DateTime.UtcNow,
Status = "draft"
});
}
}

View File

@@ -0,0 +1,58 @@
using Microsoft.Extensions.Logging;
using StellaOps.Microservice;
namespace Examples.Billing.Microservice.Endpoints;
/// <summary>
/// Request model for getting an invoice.
/// </summary>
public sealed record GetInvoiceRequest
{
public required string Id { get; init; }
}
/// <summary>
/// Response model for an invoice.
/// </summary>
public sealed record GetInvoiceResponse
{
public required string InvoiceId { get; init; }
public required string CustomerId { get; init; }
public required decimal Amount { get; init; }
public required string Status { get; init; }
public required DateTime CreatedAt { get; init; }
public DateTime? PaidAt { get; init; }
}
/// <summary>
/// Endpoint for retrieving an invoice by ID.
/// Demonstrates a GET endpoint with path parameters.
/// </summary>
[StellaEndpoint("GET", "/invoices/{id}", TimeoutSeconds = 10, RequiredClaims = ["invoices:read"])]
public sealed class GetInvoiceEndpoint : IStellaEndpoint<GetInvoiceRequest, GetInvoiceResponse>
{
private readonly ILogger<GetInvoiceEndpoint> _logger;
public GetInvoiceEndpoint(ILogger<GetInvoiceEndpoint> logger)
{
_logger = logger;
}
public Task<GetInvoiceResponse> HandleAsync(
GetInvoiceRequest request,
CancellationToken cancellationToken)
{
_logger.LogInformation("Fetching invoice {InvoiceId}", request.Id);
// Simulate invoice lookup
return Task.FromResult(new GetInvoiceResponse
{
InvoiceId = request.Id,
CustomerId = "CUST-001",
Amount = 199.99m,
Status = "paid",
CreatedAt = DateTime.UtcNow.AddDays(-7),
PaidAt = DateTime.UtcNow.AddDays(-1)
});
}
}

View File

@@ -0,0 +1,60 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;
using StellaOps.Microservice;
namespace Examples.Billing.Microservice.Endpoints;
/// <summary>
/// Endpoint for uploading attachments to an invoice.
/// Demonstrates streaming upload using IRawStellaEndpoint.
/// </summary>
[StellaEndpoint("POST", "/invoices/{id}/attachments", SupportsStreaming = true, TimeoutSeconds = 300)]
public sealed class UploadAttachmentEndpoint : IRawStellaEndpoint
{
private readonly ILogger<UploadAttachmentEndpoint> _logger;
public UploadAttachmentEndpoint(ILogger<UploadAttachmentEndpoint> logger)
{
_logger = logger;
}
public async Task<RawResponse> HandleAsync(
RawRequestContext context,
CancellationToken cancellationToken)
{
var invoiceId = context.PathParameters.GetValueOrDefault("id") ?? "unknown";
var contentType = context.Headers["Content-Type"] ?? "application/octet-stream";
_logger.LogInformation(
"Uploading attachment for invoice {InvoiceId}, Content-Type: {ContentType}",
invoiceId,
contentType);
// Read the streamed body
long totalBytes = 0;
var buffer = new byte[8192];
int bytesRead;
while ((bytesRead = await context.Body.ReadAsync(buffer, cancellationToken)) > 0)
{
totalBytes += bytesRead;
// In a real implementation, you would write to storage here
}
_logger.LogInformation(
"Received {TotalBytes} bytes for invoice {InvoiceId}",
totalBytes,
invoiceId);
// Return success response
var response = new
{
invoiceId,
attachmentId = $"ATT-{Guid.NewGuid():N}"[..16].ToUpperInvariant(),
size = totalBytes,
uploadedAt = DateTime.UtcNow
};
return RawResponse.Ok(JsonSerializer.Serialize(response));
}
}

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Microservice\StellaOps.Microservice.csproj" />
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Router.Common\StellaOps.Router.Common.csproj" />
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Router.Transport.InMemory\StellaOps.Router.Transport.InMemory.csproj" />
<!-- Reference the source generator -->
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Microservice.SourceGen\StellaOps.Microservice.SourceGen.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<Content Include="microservice.yaml" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,40 @@
using Examples.Billing.Microservice.Endpoints;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using StellaOps.Microservice;
using StellaOps.Router.Common.Enums;
using StellaOps.Router.Transport.InMemory;
var builder = Host.CreateApplicationBuilder(args);
// Configure the Stella microservice
builder.Services.AddStellaMicroservice(options =>
{
options.ServiceName = "billing";
options.Version = "1.0.0";
options.Region = "demo";
options.InstanceId = $"billing-{Environment.MachineName}";
options.ConfigFilePath = "microservice.yaml";
options.Routers =
[
new RouterEndpointConfig
{
Host = "localhost",
Port = 5100,
TransportType = TransportType.InMemory
}
];
});
// Register endpoint handlers
builder.Services.AddScoped<CreateInvoiceEndpoint>();
builder.Services.AddScoped<GetInvoiceEndpoint>();
builder.Services.AddScoped<UploadAttachmentEndpoint>();
// Add in-memory transport
builder.Services.AddInMemoryTransport();
var host = builder.Build();
Console.WriteLine("Billing microservice starting...");
await host.RunAsync();

View File

@@ -0,0 +1,21 @@
# Microservice YAML Configuration for Billing Service
# Overrides code-defined endpoint settings
endpoints:
# Override timeout for invoice creation
- method: POST
path: /invoices
timeout: 45s # Allow more time for complex invoice creation
# Override streaming settings for file upload
- method: POST
path: /invoices/{id}/attachments
timeout: 5m # Allow large file uploads
streaming: true
# Add claim requirements for getting invoices
- method: GET
path: /invoices/{id}
requiringClaims:
- type: "scope"
value: "invoices:read"

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Router.Gateway\StellaOps.Router.Gateway.csproj" />
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Router.Transport.InMemory\StellaOps.Router.Transport.InMemory.csproj" />
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Router.Config\StellaOps.Router.Config.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="router.yaml" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,50 @@
using StellaOps.Router.Gateway;
using StellaOps.Router.Gateway.Authorization;
using StellaOps.Router.Gateway.DependencyInjection;
using StellaOps.Router.Config;
using StellaOps.Router.Transport.InMemory;
var builder = WebApplication.CreateBuilder(args);
// Router configuration from YAML
builder.Services.AddRouterConfig(options =>
{
options.ConfigPath = "router.yaml";
options.EnableHotReload = true;
});
// Router gateway services
builder.Services.AddRouterGateway(builder.Configuration);
// In-memory transport for demo (can switch to TCP/TLS for production)
builder.Services.AddInMemoryTransport();
// Authority integration (no-op for demo)
builder.Services.AddNoOpAuthorityIntegration();
// Required for app.UseAuthentication() even when running without a real auth scheme (demo/tests).
builder.Services.AddAuthentication();
var app = builder.Build();
// Middleware pipeline
app.UseForwardedHeaders();
app.UseAuthentication();
app.UseClaimsAuthorization();
// Map OpenAPI endpoints
app.MapRouterOpenApi();
// Simple health endpoint
app.MapGet("/health", () => Results.Ok(new { status = "healthy" }));
// Router gateway middleware (endpoint resolution, routing decision, dispatch)
app.UseRouterGateway();
app.Run();
// Partial class for WebApplicationFactory integration testing
namespace Examples.Gateway
{
public partial class Program { }
}

View File

@@ -0,0 +1,13 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"GatewayNode": {
"Region": "demo",
"NodeId": "gw-demo-01"
}
}

View File

@@ -0,0 +1,50 @@
# Router Configuration for Example Gateway
# This file configures how the gateway routes requests to microservices
gateway:
nodeId: "gw-demo-01"
region: "demo"
listenPort: 8080
# Payload limits
payloadLimits:
maxRequestBodyBytes: 10485760 # 10 MB
maxStreamingChunkBytes: 65536 # 64 KB
# Health monitoring
healthMonitoring:
staleThreshold: "00:00:30"
checkInterval: "00:00:05"
# Transport configuration
transports:
# In-memory transport (for demo)
inMemory:
enabled: true
# TCP transport (production)
# tcp:
# enabled: true
# port: 5100
# backlog: 100
# TLS transport (production with encryption)
# tls:
# enabled: true
# port: 5101
# certificatePath: "certs/gateway.pfx"
# certificatePassword: "demo"
# Routing configuration
routing:
# Default routing algorithm
algorithm: "round-robin"
# Region affinity (prefer local microservices)
regionAffinity: true
affinityWeight: 0.8
# Logging
logging:
level: "Information"
requestLogging: true

View File

@@ -0,0 +1,64 @@
using Microsoft.Extensions.Logging;
using StellaOps.Microservice;
namespace Examples.Inventory.Microservice.Endpoints;
/// <summary>
/// Request model for getting a single inventory item.
/// </summary>
public sealed record GetItemRequest
{
public required string Sku { get; init; }
}
/// <summary>
/// Response model for a single inventory item with details.
/// </summary>
public sealed record GetItemResponse
{
public required string Sku { get; init; }
public required string Name { get; init; }
public required string Description { get; init; }
public required string Category { get; init; }
public required int QuantityOnHand { get; init; }
public required int ReorderPoint { get; init; }
public required decimal UnitPrice { get; init; }
public required string Location { get; init; }
public required DateTime LastUpdated { get; init; }
}
/// <summary>
/// Endpoint for getting a single inventory item by SKU.
/// Demonstrates path parameter extraction.
/// </summary>
[StellaEndpoint("GET", "/items/{sku}", TimeoutSeconds = 10)]
public sealed class GetItemEndpoint : IStellaEndpoint<GetItemRequest, GetItemResponse>
{
private readonly ILogger<GetItemEndpoint> _logger;
public GetItemEndpoint(ILogger<GetItemEndpoint> logger)
{
_logger = logger;
}
public Task<GetItemResponse> HandleAsync(
GetItemRequest request,
CancellationToken cancellationToken)
{
_logger.LogInformation("Fetching inventory item {Sku}", request.Sku);
// Simulate item lookup
return Task.FromResult(new GetItemResponse
{
Sku = request.Sku,
Name = "Widget A",
Description = "A high-quality widget for general purpose use",
Category = "widgets",
QuantityOnHand = 100,
ReorderPoint = 25,
UnitPrice = 9.99m,
Location = "Warehouse A, Aisle 3, Shelf 2",
LastUpdated = DateTime.UtcNow.AddHours(-2)
});
}
}

View File

@@ -0,0 +1,107 @@
using Microsoft.Extensions.Logging;
using StellaOps.Microservice;
namespace Examples.Inventory.Microservice.Endpoints;
/// <summary>
/// Request model for listing inventory items.
/// </summary>
public sealed record ListItemsRequest
{
public int Page { get; init; } = 1;
public int PageSize { get; init; } = 20;
public string? Category { get; init; }
}
/// <summary>
/// Response model for listing inventory items.
/// </summary>
public sealed record ListItemsResponse
{
public required List<InventoryItem> Items { get; init; }
public required int TotalCount { get; init; }
public required int Page { get; init; }
public required int PageSize { get; init; }
}
/// <summary>
/// Inventory item model.
/// </summary>
public sealed record InventoryItem
{
public required string Sku { get; init; }
public required string Name { get; init; }
public required string Category { get; init; }
public required int QuantityOnHand { get; init; }
public required decimal UnitPrice { get; init; }
}
/// <summary>
/// Endpoint for listing inventory items.
/// Demonstrates pagination and filtering.
/// </summary>
[StellaEndpoint("GET", "/items", TimeoutSeconds = 15)]
public sealed class ListItemsEndpoint : IStellaEndpoint<ListItemsRequest, ListItemsResponse>
{
private readonly ILogger<ListItemsEndpoint> _logger;
public ListItemsEndpoint(ILogger<ListItemsEndpoint> logger)
{
_logger = logger;
}
public Task<ListItemsResponse> HandleAsync(
ListItemsRequest request,
CancellationToken cancellationToken)
{
_logger.LogInformation(
"Listing inventory items - Page: {Page}, PageSize: {PageSize}, Category: {Category}",
request.Page,
request.PageSize,
request.Category ?? "(all)");
// Simulate item list
var items = new List<InventoryItem>
{
new()
{
Sku = "SKU-001",
Name = "Widget A",
Category = "widgets",
QuantityOnHand = 100,
UnitPrice = 9.99m
},
new()
{
Sku = "SKU-002",
Name = "Widget B",
Category = "widgets",
QuantityOnHand = 50,
UnitPrice = 14.99m
},
new()
{
Sku = "SKU-003",
Name = "Gadget X",
Category = "gadgets",
QuantityOnHand = 25,
UnitPrice = 29.99m
}
};
// Filter by category if specified
if (!string.IsNullOrWhiteSpace(request.Category))
{
items = items.Where(i =>
i.Category.Equals(request.Category, StringComparison.OrdinalIgnoreCase)).ToList();
}
return Task.FromResult(new ListItemsResponse
{
Items = items,
TotalCount = items.Count,
Page = request.Page,
PageSize = request.PageSize
});
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Microservice\StellaOps.Microservice.csproj" />
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Router.Common\StellaOps.Router.Common.csproj" />
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Router.Transport.InMemory\StellaOps.Router.Transport.InMemory.csproj" />
<!-- Reference the source generator -->
<ProjectReference Include="..\..\..\..\src\__Libraries\StellaOps.Microservice.SourceGen\StellaOps.Microservice.SourceGen.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,38 @@
using Examples.Inventory.Microservice.Endpoints;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using StellaOps.Microservice;
using StellaOps.Router.Common.Enums;
using StellaOps.Router.Transport.InMemory;
var builder = Host.CreateApplicationBuilder(args);
// Configure the Stella microservice
builder.Services.AddStellaMicroservice(options =>
{
options.ServiceName = "inventory";
options.Version = "1.0.0";
options.Region = "demo";
options.InstanceId = $"inventory-{Environment.MachineName}";
options.Routers =
[
new RouterEndpointConfig
{
Host = "localhost",
Port = 5100,
TransportType = TransportType.InMemory
}
];
});
// Register endpoint handlers
builder.Services.AddScoped<ListItemsEndpoint>();
builder.Services.AddScoped<GetItemEndpoint>();
// Add in-memory transport
builder.Services.AddInMemoryTransport();
var host = builder.Build();
Console.WriteLine("Inventory microservice starting...");
await host.RunAsync();

View File

@@ -0,0 +1,76 @@
using System.Net;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using FluentAssertions;
using Xunit;
namespace Examples.Integration.Tests;
/// <summary>
/// Integration tests for the Billing microservice endpoints.
/// </summary>
public sealed class BillingEndpointTests : IClassFixture<GatewayFixture>
{
private readonly GatewayFixture _fixture;
public BillingEndpointTests(GatewayFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task CreateInvoice_WithValidRequest_ReturnsCreatedInvoice()
{
// Arrange
var request = new
{
customerId = "CUST-001",
amount = 99.99m,
description = "Test invoice"
};
// Act
var response = await _fixture.GatewayClient.PostAsJsonAsync("/invoices", request);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
content.Should().Contain("invoiceId");
}
[Fact]
public async Task GetInvoice_WithValidId_ReturnsInvoice()
{
// Arrange
var invoiceId = "INV-12345";
// Act
var response = await _fixture.GatewayClient.GetAsync($"/invoices/{invoiceId}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
content.Should().Contain(invoiceId);
}
[Fact]
public async Task UploadAttachment_WithStreamingData_ReturnsSuccess()
{
// Arrange
var invoiceId = "INV-12345";
var attachmentData = Encoding.UTF8.GetBytes("This is test attachment content");
using var content = new ByteArrayContent(attachmentData);
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
// Act
var response = await _fixture.GatewayClient.PostAsync(
$"/invoices/{invoiceId}/attachments",
content);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var responseContent = await response.Content.ReadAsStringAsync();
responseContent.Should().Contain("attachmentId");
}
}

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Examples.Gateway\Examples.Gateway.csproj" />
<ProjectReference Include="..\..\src\Examples.Billing.Microservice\Examples.Billing.Microservice.csproj" />
<ProjectReference Include="..\..\src\Examples.Inventory.Microservice\Examples.Inventory.Microservice.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,147 @@
using Examples.Billing.Microservice.Endpoints;
using Examples.Inventory.Microservice.Endpoints;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using StellaOps.Microservice;
using StellaOps.Router.Common.Abstractions;
using StellaOps.Router.Common.Enums;
using StellaOps.Router.Transport.InMemory;
using Xunit;
namespace Examples.Integration.Tests;
/// <summary>
/// Test fixture that sets up the gateway and microservices for integration testing.
/// Uses in-memory transport for fast, isolated tests.
/// </summary>
public sealed class GatewayFixture : IAsyncLifetime
{
private readonly InMemoryConnectionRegistry _registry = new();
private WebApplicationFactory<Examples.Gateway.Program>? _gatewayFactory;
private IHost? _billingHost;
private IHost? _inventoryHost;
public HttpClient GatewayClient { get; private set; } = null!;
public async Task InitializeAsync()
{
// Start the gateway
_gatewayFactory = new WebApplicationFactory<Examples.Gateway.Program>()
.WithWebHostBuilder(builder =>
{
builder.UseEnvironment("Testing");
builder.ConfigureServices(services =>
{
services.RemoveAll<InMemoryConnectionRegistry>();
services.AddSingleton(_registry);
});
});
GatewayClient = _gatewayFactory.CreateClient();
// Start billing microservice
var billingBuilder = Host.CreateApplicationBuilder();
billingBuilder.Services.AddStellaMicroservice(options =>
{
options.ServiceName = "billing";
options.Version = "1.0.0";
options.Region = "test";
options.InstanceId = "billing-test";
options.Routers =
[
new RouterEndpointConfig
{
Host = "localhost",
Port = 5100,
TransportType = TransportType.InMemory
}
];
});
billingBuilder.Services.AddScoped<CreateInvoiceEndpoint>();
billingBuilder.Services.AddScoped<GetInvoiceEndpoint>();
billingBuilder.Services.AddScoped<UploadAttachmentEndpoint>();
billingBuilder.Services.AddSingleton(_registry);
billingBuilder.Services.AddInMemoryTransportClient();
_billingHost = billingBuilder.Build();
await _billingHost.StartAsync();
// Start inventory microservice
var inventoryBuilder = Host.CreateApplicationBuilder();
inventoryBuilder.Services.AddStellaMicroservice(options =>
{
options.ServiceName = "inventory";
options.Version = "1.0.0";
options.Region = "test";
options.InstanceId = "inventory-test";
options.Routers =
[
new RouterEndpointConfig
{
Host = "localhost",
Port = 5100,
TransportType = TransportType.InMemory
}
];
});
inventoryBuilder.Services.AddScoped<ListItemsEndpoint>();
inventoryBuilder.Services.AddScoped<GetItemEndpoint>();
inventoryBuilder.Services.AddSingleton(_registry);
inventoryBuilder.Services.AddInMemoryTransportClient();
_inventoryHost = inventoryBuilder.Build();
await _inventoryHost.StartAsync();
await WaitForGatewayReadyAsync(TimeSpan.FromSeconds(5));
}
public async Task DisposeAsync()
{
GatewayClient.Dispose();
if (_billingHost is not null)
{
await _billingHost.StopAsync();
_billingHost.Dispose();
}
if (_inventoryHost is not null)
{
await _inventoryHost.StopAsync();
_inventoryHost.Dispose();
}
_gatewayFactory?.Dispose();
}
private async Task WaitForGatewayReadyAsync(TimeSpan timeout)
{
if (_gatewayFactory is null)
{
throw new InvalidOperationException("Gateway factory not initialized.");
}
var routingState = _gatewayFactory.Services.GetRequiredService<IGlobalRoutingState>();
var deadline = DateTimeOffset.UtcNow.Add(timeout);
while (DateTimeOffset.UtcNow < deadline)
{
var connections = routingState.GetAllConnections();
if (connections.Count >= 2 &&
routingState.ResolveEndpoint("GET", "/items") is not null &&
routingState.ResolveEndpoint("POST", "/invoices") is not null)
{
return;
}
await Task.Delay(50);
}
var currentConnections = routingState.GetAllConnections();
throw new TimeoutException(
$"Gateway routing state not ready after {timeout}. Connections={currentConnections.Count}.");
}
}

View File

@@ -0,0 +1,74 @@
using System.Net;
using System.Text.Json;
using FluentAssertions;
using Xunit;
namespace Examples.Integration.Tests;
/// <summary>
/// Integration tests for the Inventory microservice endpoints.
/// </summary>
public sealed class InventoryEndpointTests : IClassFixture<GatewayFixture>
{
private readonly GatewayFixture _fixture;
public InventoryEndpointTests(GatewayFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task ListItems_WithoutFilters_ReturnsAllItems()
{
// Act
var response = await _fixture.GatewayClient.GetAsync("/items");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
content.Should().Contain("items");
content.Should().Contain("totalCount");
}
[Fact]
public async Task ListItems_WithCategoryFilter_ReturnsFilteredItems()
{
// Act
var response = await _fixture.GatewayClient.GetAsync("/items?category=widgets");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
content.Should().Contain("widgets");
}
[Fact]
public async Task ListItems_WithPagination_ReturnsPaginatedResponse()
{
// Act
var response = await _fixture.GatewayClient.GetAsync("/items?page=1&pageSize=10");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
content.Should().Contain("\"page\":1");
content.Should().Contain("\"pageSize\":10");
}
[Fact]
public async Task GetItem_WithValidSku_ReturnsItem()
{
// Arrange
var sku = "SKU-001";
// Act
var response = await _fixture.GatewayClient.GetAsync($"/items/{sku}");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
content.Should().Contain(sku);
content.Should().Contain("name");
content.Should().Contain("quantityOnHand");
}
}

View File

@@ -0,0 +1,79 @@
using System.Net;
using FluentAssertions;
using Xunit;
namespace Examples.Integration.Tests;
/// <summary>
/// Tests that verify multiple microservices can register and receive
/// correctly routed requests through the gateway.
/// </summary>
public sealed class MultiServiceRoutingTests : IClassFixture<GatewayFixture>
{
private readonly GatewayFixture _fixture;
public MultiServiceRoutingTests(GatewayFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task Gateway_RoutesBillingRequests_ToBillingService()
{
// Act
var response = await _fixture.GatewayClient.GetAsync("/invoices/INV-001");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
content.Should().Contain("INV-001");
}
[Fact]
public async Task Gateway_RoutesInventoryRequests_ToInventoryService()
{
// Act
var response = await _fixture.GatewayClient.GetAsync("/items/SKU-001");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
content.Should().Contain("SKU-001");
}
[Fact]
public async Task Gateway_HandlesSequentialRequestsToDifferentServices()
{
// Act - Send requests to both services
var billingResponse = await _fixture.GatewayClient.GetAsync("/invoices/INV-001");
var inventoryResponse = await _fixture.GatewayClient.GetAsync("/items/SKU-001");
// Assert - Both should succeed
billingResponse.StatusCode.Should().Be(HttpStatusCode.OK);
inventoryResponse.StatusCode.Should().Be(HttpStatusCode.OK);
}
[Fact]
public async Task Gateway_HandlesConcurrentRequestsToDifferentServices()
{
// Act - Send requests to both services concurrently
var billingTask = _fixture.GatewayClient.GetAsync("/invoices/INV-001");
var inventoryTask = _fixture.GatewayClient.GetAsync("/items/SKU-001");
await Task.WhenAll(billingTask, inventoryTask);
// Assert - Both should succeed
billingTask.Result.StatusCode.Should().Be(HttpStatusCode.OK);
inventoryTask.Result.StatusCode.Should().Be(HttpStatusCode.OK);
}
[Fact]
public async Task Gateway_ReturnsNotFound_ForUnknownRoute()
{
// Act
var response = await _fixture.GatewayClient.GetAsync("/unknown/route");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
}

View File

@@ -0,0 +1,203 @@
# Router Transport Plugins
StellaOps Router uses a **plugin-based transport architecture** that enables runtime loading of transport implementations. This allows operators to deploy only the transports they need and swap implementations without recompiling the Gateway.
## Available Transports
| Transport | Plugin Assembly | Use Case | Status |
|-----------|-----------------|----------|--------|
| [TCP](./tcp.md) | `StellaOps.Router.Transport.Tcp.dll` | Internal services, same datacenter | Stable |
| [TLS](./tls.md) | `StellaOps.Router.Transport.Tls.dll` | Cross-datacenter, mTLS | Stable |
| [UDP](./udp.md) | `StellaOps.Router.Transport.Udp.dll` | Fire-and-forget, broadcast | Stable |
| [RabbitMQ](./rabbitmq.md) | `StellaOps.Router.Transport.RabbitMq.dll` | Async processing, fan-out | Stable |
| [InMemory](./inmemory.md) | `StellaOps.Router.Transport.InMemory.dll` | Development, testing | Stable |
| Valkey | `StellaOps.Messaging.Transport.Valkey.dll` | Distributed, pub/sub | Stable |
| PostgreSQL | `StellaOps.Messaging.Transport.Postgres.dll` | Transactional, LISTEN/NOTIFY | Stable |
## Plugin Architecture
### Loading Model
Transport plugins are loaded at Gateway startup via `RouterTransportPluginLoader`:
```
┌─────────────────────────────────────────────────────────────────┐
│ Gateway Startup │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ RouterTransportPluginLoader │ │
│ │ │ │
│ │ 1. Scan plugins/router/transports/ │ │
│ │ 2. Load assemblies in isolation (AssemblyLoadContext) │ │
│ │ 3. Discover IRouterTransportPlugin implementations │ │
│ │ 4. Register configured transport with DI │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Transport Plugin (e.g., TLS) │ │
│ │ │ │
│ │ - TransportName: "tls" │ │
│ │ - DisplayName: "TLS Transport" │ │
│ │ - IsAvailable(): Check dependencies │ │
│ │ - Register(): Wire up ITransportServer/ITransportClient │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
```
### Directory Structure
```
plugins/
└── router/
└── transports/
├── StellaOps.Router.Transport.Tcp.dll
├── StellaOps.Router.Transport.Tls.dll
├── StellaOps.Router.Transport.Udp.dll
├── StellaOps.Router.Transport.RabbitMq.dll
└── StellaOps.Router.Transport.InMemory.dll
```
### Configuration
Transport selection and options are configured in `router.yaml` or environment variables:
```yaml
Router:
Transport:
Type: tls # Which transport plugin to use
Tls: # Transport-specific options
Port: 5101
CertificatePath: /certs/server.pfx
RequireClientCertificate: true
Tcp:
Port: 5100
MaxConnections: 1000
```
Environment override:
```bash
ROUTER__TRANSPORT__TYPE=tcp
ROUTER__TRANSPORT__TCP__PORT=5100
```
## Using Plugins in Gateway
### Programmatic Loading
```csharp
using StellaOps.Router.Common.Plugins;
var builder = WebApplication.CreateBuilder(args);
// Load transport plugins from directory
var pluginLoader = new RouterTransportPluginLoader(
builder.Services.BuildServiceProvider().GetService<ILogger<RouterTransportPluginLoader>>());
var pluginsPath = Path.Combine(AppContext.BaseDirectory, "plugins", "router", "transports");
pluginLoader.LoadFromDirectory(pluginsPath);
// Register the configured transport (reads Router:Transport:Type from config)
pluginLoader.RegisterConfiguredTransport(
builder.Services,
builder.Configuration,
RouterTransportMode.Both); // Register both server and client
var app = builder.Build();
// ...
```
### Gateway Integration
The Gateway automatically loads transport plugins during startup. Configure in `router.yaml`:
```yaml
gateway:
name: api-gateway
plugins:
transports:
directory: plugins/router/transports
searchPattern: "StellaOps.Router.Transport.*.dll"
```
## Creating Custom Transports
See the [Transport Plugin Development Guide](./development.md) for creating custom transport implementations.
### Minimal Plugin Example
```csharp
using StellaOps.Router.Common.Plugins;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace MyCompany.Router.Transport.Custom;
public sealed class CustomTransportPlugin : IRouterTransportPlugin
{
public string TransportName => "custom";
public string DisplayName => "Custom Transport";
public bool IsAvailable(IServiceProvider services) => true;
public void Register(RouterTransportRegistrationContext context)
{
var configSection = context.Configuration.GetSection("Router:Transport:Custom");
context.Services.Configure<CustomTransportOptions>(options =>
{
configSection.Bind(options);
});
if (context.Mode.HasFlag(RouterTransportMode.Server))
{
context.Services.AddSingleton<CustomTransportServer>();
context.Services.AddSingleton<ITransportServer>(sp =>
sp.GetRequiredService<CustomTransportServer>());
}
if (context.Mode.HasFlag(RouterTransportMode.Client))
{
context.Services.AddSingleton<CustomTransportClient>();
context.Services.AddSingleton<ITransportClient>(sp =>
sp.GetRequiredService<CustomTransportClient>());
context.Services.AddSingleton<IMicroserviceTransport>(sp =>
sp.GetRequiredService<CustomTransportClient>());
}
}
}
```
## Transport Selection Guide
| Scenario | Recommended Transport | Configuration |
|----------|----------------------|---------------|
| Development/Testing | InMemory | `Type: inmemory` |
| Same-datacenter | TCP | `Type: tcp` |
| Cross-datacenter secure | TLS | `Type: tls` with mTLS |
| High-volume async | RabbitMQ | `Type: rabbitmq` |
| Broadcast/fire-and-forget | UDP | `Type: udp` |
| Distributed with replay | Valkey | Via Messaging plugins |
| Transactional messaging | PostgreSQL | Via Messaging plugins |
## Air-Gap Deployment
For offline/air-gapped deployments:
1. Pre-package transport plugins with your deployment
2. Configure the plugin directory path
3. No external network access required
```yaml
gateway:
plugins:
transports:
directory: /opt/stellaops/plugins/router/transports
```
## See Also
- [Router Architecture](../ARCHITECTURE.md)
- [Plugin SDK Guide](../../10_PLUGIN_SDK_GUIDE.md)
- [Unified Plugin System](../../plugins/README.md)

View File

@@ -0,0 +1,534 @@
# Transport Plugin Development Guide
This guide explains how to create custom router transport plugins for StellaOps.
## Overview
Router transport plugins implement the `IRouterTransportPlugin` interface to provide custom communication protocols. The plugin system enables:
- Runtime loading of transport implementations
- Isolation from the Gateway codebase
- Hot-swappable transports (restart required)
- Third-party transport extensions
## Prerequisites
- .NET 10 SDK
- Understanding of async socket programming
- Familiarity with dependency injection
## Creating a Transport Plugin
### Step 1: Create Project
```bash
mkdir MyCompany.Router.Transport.Custom
cd MyCompany.Router.Transport.Custom
dotnet new classlib -f net10.0
dotnet add package Microsoft.Extensions.Configuration.Binder
dotnet add package Microsoft.Extensions.DependencyInjection.Abstractions
dotnet add package Microsoft.Extensions.Logging.Abstractions
dotnet add package Microsoft.Extensions.Options
```
Add project reference to Router.Common:
```xml
<ItemGroup>
<ProjectReference Include="path/to/StellaOps.Router.Common/StellaOps.Router.Common.csproj" />
</ItemGroup>
```
### Step 2: Create Options Class
```csharp
namespace MyCompany.Router.Transport.Custom;
/// <summary>
/// Configuration options for the custom transport.
/// </summary>
public sealed class CustomTransportOptions
{
/// <summary>
/// Host address to bind/connect to.
/// </summary>
public string Host { get; set; } = "0.0.0.0";
/// <summary>
/// Port number.
/// </summary>
public int Port { get; set; } = 5200;
/// <summary>
/// Connection timeout.
/// </summary>
public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Maximum concurrent connections.
/// </summary>
public int MaxConnections { get; set; } = 1000;
}
```
### Step 3: Implement Transport Server
```csharp
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Router.Common.Abstractions;
namespace MyCompany.Router.Transport.Custom;
public sealed class CustomTransportServer : ITransportServer
{
private readonly CustomTransportOptions _options;
private readonly ILogger<CustomTransportServer> _logger;
public CustomTransportServer(
IOptions<CustomTransportOptions> options,
ILogger<CustomTransportServer> logger)
{
_options = options.Value;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting custom transport server on {Host}:{Port}",
_options.Host, _options.Port);
// Initialize your transport server (socket, listener, etc.)
// ...
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Stopping custom transport server");
// Graceful shutdown
// ...
}
public async Task<ITransportConnection> AcceptAsync(CancellationToken cancellationToken)
{
// Accept incoming connection
// Return ITransportConnection implementation
throw new NotImplementedException();
}
public void Dispose()
{
// Cleanup resources
}
}
```
### Step 4: Implement Transport Client
```csharp
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Router.Common.Abstractions;
namespace MyCompany.Router.Transport.Custom;
public sealed class CustomTransportClient : ITransportClient, IMicroserviceTransport
{
private readonly CustomTransportOptions _options;
private readonly ILogger<CustomTransportClient> _logger;
public CustomTransportClient(
IOptions<CustomTransportOptions> options,
ILogger<CustomTransportClient> logger)
{
_options = options.Value;
_logger = logger;
}
public async Task<ITransportConnection> ConnectAsync(
string host,
int port,
CancellationToken cancellationToken)
{
_logger.LogDebug("Connecting to {Host}:{Port}", host, port);
// Establish connection
// Return ITransportConnection implementation
throw new NotImplementedException();
}
public async Task DisconnectAsync(CancellationToken cancellationToken)
{
// Disconnect from server
}
public void Dispose()
{
// Cleanup resources
}
}
```
### Step 5: Implement Connection
```csharp
using StellaOps.Router.Common.Abstractions;
namespace MyCompany.Router.Transport.Custom;
public sealed class CustomTransportConnection : ITransportConnection
{
public string ConnectionId { get; } = Guid.NewGuid().ToString("N");
public bool IsConnected { get; private set; }
public EndPoint? RemoteEndPoint { get; private set; }
public async Task<int> SendAsync(
ReadOnlyMemory<byte> data,
CancellationToken cancellationToken)
{
// Send data over transport
throw new NotImplementedException();
}
public async Task<int> ReceiveAsync(
Memory<byte> buffer,
CancellationToken cancellationToken)
{
// Receive data from transport
throw new NotImplementedException();
}
public async Task CloseAsync(CancellationToken cancellationToken)
{
IsConnected = false;
// Close connection
}
public void Dispose()
{
// Cleanup
}
}
```
### Step 6: Implement Plugin
```csharp
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Router.Common.Abstractions;
using StellaOps.Router.Common.Plugins;
namespace MyCompany.Router.Transport.Custom;
/// <summary>
/// Plugin implementation for custom transport.
/// </summary>
public sealed class CustomTransportPlugin : IRouterTransportPlugin
{
/// <inheritdoc />
public string TransportName => "custom";
/// <inheritdoc />
public string DisplayName => "Custom Transport";
/// <inheritdoc />
public bool IsAvailable(IServiceProvider services)
{
// Check if required dependencies are available
// Return false if transport cannot be used in current environment
return true;
}
/// <inheritdoc />
public void Register(RouterTransportRegistrationContext context)
{
var services = context.Services;
var configuration = context.Configuration;
// Bind configuration
var configSection = context.ConfigurationSection is not null
? configuration.GetSection(context.ConfigurationSection)
: configuration.GetSection("Router:Transport:Custom");
services.AddOptions<CustomTransportOptions>();
if (configSection.GetChildren().Any())
{
services.Configure<CustomTransportOptions>(options =>
{
configSection.Bind(options);
});
}
// Register server if requested
if (context.Mode.HasFlag(RouterTransportMode.Server))
{
services.AddSingleton<CustomTransportServer>();
services.AddSingleton<ITransportServer>(sp =>
sp.GetRequiredService<CustomTransportServer>());
}
// Register client if requested
if (context.Mode.HasFlag(RouterTransportMode.Client))
{
services.AddSingleton<CustomTransportClient>();
services.AddSingleton<ITransportClient>(sp =>
sp.GetRequiredService<CustomTransportClient>());
services.AddSingleton<IMicroserviceTransport>(sp =>
sp.GetRequiredService<CustomTransportClient>());
}
}
}
```
## Building and Packaging
### Build Configuration
Add to your `.csproj`:
```xml
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>MyCompany.Router.Transport.Custom</RootNamespace>
<!-- Plugin assembly attributes -->
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
</PropertyGroup>
<!-- Output to plugins directory -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<OutputPath>$(SolutionDir)plugins\router\transports\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
```
### Build Commands
```bash
# Debug build
dotnet build
# Release build to plugins directory
dotnet build -c Release
# Publish with all dependencies
dotnet publish -c Release -o ./publish
```
## Configuration Schema
Create `plugin.json` manifest:
```json
{
"schemaVersion": "2.0",
"id": "mycompany.router.transport.custom",
"name": "Custom Transport",
"version": "1.0.0",
"assembly": {
"path": "MyCompany.Router.Transport.Custom.dll",
"entryType": "MyCompany.Router.Transport.Custom.CustomTransportPlugin"
},
"capabilities": ["server", "client", "streaming"],
"platforms": ["linux-x64", "win-x64", "osx-arm64"],
"enabled": true,
"priority": 100
}
```
Create `config.yaml` for runtime configuration:
```yaml
id: mycompany.router.transport.custom
name: Custom Transport
enabled: true
config:
host: "0.0.0.0"
port: 5200
maxConnections: 1000
connectTimeout: "00:00:30"
```
## Testing
### Unit Tests
```csharp
public class CustomTransportPluginTests
{
[Fact]
public void TransportName_ReturnsCustom()
{
var plugin = new CustomTransportPlugin();
Assert.Equal("custom", plugin.TransportName);
}
[Fact]
public void Register_AddsServerServices()
{
var plugin = new CustomTransportPlugin();
var services = new ServiceCollection();
var config = new ConfigurationBuilder().Build();
var context = new RouterTransportRegistrationContext(
services, config, RouterTransportMode.Server);
plugin.Register(context);
var provider = services.BuildServiceProvider();
Assert.NotNull(provider.GetService<ITransportServer>());
}
}
```
### Integration Tests
```csharp
public class CustomTransportIntegrationTests
{
[Fact]
public async Task Server_AcceptsConnections()
{
var services = new ServiceCollection();
services.AddLogging();
services.Configure<CustomTransportOptions>(opts =>
{
opts.Port = 15200; // Test port
});
services.AddSingleton<CustomTransportServer>();
var provider = services.BuildServiceProvider();
var server = provider.GetRequiredService<CustomTransportServer>();
await server.StartAsync(CancellationToken.None);
// Connect client and verify
// ...
await server.StopAsync(CancellationToken.None);
}
}
```
## Best Practices
### Error Handling
```csharp
public async Task<ITransportConnection> ConnectAsync(
string host,
int port,
CancellationToken cancellationToken)
{
try
{
// Attempt connection
return await ConnectInternalAsync(host, port, cancellationToken);
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused)
{
_logger.LogWarning("Connection refused to {Host}:{Port}", host, port);
throw new TransportConnectionException($"Connection refused to {host}:{port}", ex);
}
catch (OperationCanceledException)
{
_logger.LogDebug("Connection attempt cancelled");
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to connect to {Host}:{Port}", host, port);
throw new TransportConnectionException($"Failed to connect to {host}:{port}", ex);
}
}
```
### Resource Management
```csharp
public sealed class CustomTransportServer : ITransportServer, IAsyncDisposable
{
private readonly SemaphoreSlim _connectionLock = new(1, 1);
private readonly List<ITransportConnection> _connections = [];
private bool _disposed;
public async ValueTask DisposeAsync()
{
if (_disposed) return;
_disposed = true;
await _connectionLock.WaitAsync();
try
{
foreach (var conn in _connections)
{
await conn.CloseAsync(CancellationToken.None);
conn.Dispose();
}
_connections.Clear();
}
finally
{
_connectionLock.Release();
_connectionLock.Dispose();
}
}
}
```
### Logging
```csharp
// Use structured logging
_logger.LogInformation(
"Connection established {ConnectionId} from {RemoteEndPoint}",
connection.ConnectionId,
connection.RemoteEndPoint);
// Include correlation IDs
using (_logger.BeginScope(new Dictionary<string, object>
{
["ConnectionId"] = connectionId,
["TraceId"] = Activity.Current?.TraceId.ToString() ?? "N/A"
}))
{
await ProcessConnectionAsync(connection, cancellationToken);
}
```
## Deployment
### Copy to Plugins Directory
```bash
cp ./publish/*.dll /opt/stellaops/plugins/router/transports/
```
### Verify Plugin Loading
```bash
# Check logs for plugin discovery
grep "Loaded router transport plugin" /var/log/stellaops/gateway.log
```
### Configuration
```yaml
# router.yaml
Router:
Transport:
Type: custom
Custom:
Host: "0.0.0.0"
Port: 5200
```
## See Also
- [Transport Overview](./README.md)
- [Plugin SDK Guide](../../10_PLUGIN_SDK_GUIDE.md)
- [IRouterTransportPlugin API](../../../src/Router/__Libraries/StellaOps.Router.Common/Plugins/IRouterTransportPlugin.cs)

View File

@@ -0,0 +1,233 @@
# InMemory Transport
The InMemory transport provides zero-latency, in-process communication for development, testing, and scenarios where services run in the same process.
## Overview
| Property | Value |
|----------|-------|
| Plugin Assembly | `StellaOps.Router.Transport.InMemory.dll` |
| Transport Name | `inmemory` |
| Latency | Sub-microsecond |
| Use Case | Development, testing, embedded scenarios |
## Configuration
### router.yaml
```yaml
Router:
Transport:
Type: inmemory
InMemory:
MaxPendingMessages: 10000
MessageTimeout: "00:01:00"
```
### microservice.yaml
```yaml
routers:
- host: localhost
port: 0 # Port ignored for InMemory
transportType: InMemory
priority: 1
```
### Environment Variables
```bash
ROUTER__TRANSPORT__TYPE=inmemory
```
## Options Reference
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `MaxPendingMessages` | int | `10000` | Maximum queued messages before backpressure |
| `MessageTimeout` | TimeSpan | `00:01:00` | Timeout for pending messages |
| `PreserveMessageOrder` | bool | `true` | Guarantee message ordering |
## Architecture
The InMemory transport uses a shared `InMemoryConnectionRegistry` singleton that enables direct in-process communication:
```
┌─────────────────────────────────────────────────────────────────┐
│ Single Process │
│ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ InMemoryConnectionRegistry ││
│ │ (Singleton) ││
│ │ ││
│ │ ┌────────────────┐ ┌─────────────────────────────┐ ││
│ │ │ Service A │ │ Service B │ ││
│ │ │ (InMemoryClient│◄────►│ (InMemoryServer) │ ││
│ │ │ endpoints) │ │ │ ││
│ │ └────────────────┘ └─────────────────────────────┘ ││
│ │ ││
│ │ Messages passed directly via ││
│ │ ConcurrentQueue<T> - no serialization ││
│ └──────────────────────────────────────────────────────────────┘│
│ │
└──────────────────────────────────────────────────────────────────┘
```
## Use Cases
### Development
Run Gateway and microservices in the same process:
```csharp
var builder = WebApplication.CreateBuilder(args);
// Register InMemory transport (shared between gateway and services)
builder.Services.AddInMemoryTransport();
// Add gateway
builder.Services.AddRouterGateway(builder.Configuration);
// Add microservice in same process
builder.Services.AddStellaMicroservice(options =>
{
options.ServiceName = "order-service";
options.Version = "1.0.0";
});
var app = builder.Build();
app.UseRouterGateway();
app.Run();
```
### Integration Testing
```csharp
public class OrderServiceTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public OrderServiceTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Use InMemory transport for tests
services.Configure<RouterOptions>(opts =>
opts.Transport.Type = "inmemory");
});
});
}
[Fact]
public async Task CreateOrder_ReturnsOrderId()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/orders", new
{
CustomerId = "CUST-001",
Amount = 99.99m
});
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<OrderResponse>();
Assert.NotNull(result?.OrderId);
}
}
```
### Embedded Scenarios
For single-binary deployments:
```csharp
// All services compiled into one executable
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddInMemoryTransport();
// Register all microservice handlers
services.AddScoped<CreateOrderEndpoint>();
services.AddScoped<GetOrderEndpoint>();
services.AddScoped<CreateInvoiceEndpoint>();
})
.Build();
```
## Performance Characteristics
| Metric | Typical Value |
|--------|---------------|
| Latency (p50) | < 0.1ms |
| Latency (p99) | < 0.5ms |
| Throughput | 500,000+ rps |
| Memory overhead | Minimal |
*Zero serialization, direct object passing*
## Limitations
1. **Single process only**: Cannot communicate across process boundaries
2. **No persistence**: Messages lost on process termination
3. **No distribution**: Cannot scale to multiple nodes
4. **Shared memory**: Large messages consume process memory
## When to Use InMemory
| Scenario | Use InMemory? |
|----------|---------------|
| Local development | Yes |
| Unit testing | Yes |
| Integration testing | Yes |
| Single-binary deployment | Yes |
| Multi-node deployment | No - use TCP/TLS |
| Production load testing | No - use production transport |
## Transitioning to Production
When moving from development to production:
```yaml
# Development (appsettings.Development.json)
Router:
Transport:
Type: inmemory
# Production (appsettings.Production.json)
Router:
Transport:
Type: tls
Tls:
Port: 5101
CertificatePath: /certs/server.pfx
```
No code changes required - just configuration.
## Troubleshooting
### Messages Not Being Delivered
1. Verify both client and server use InMemory transport
2. Check `InMemoryConnectionRegistry` is registered as singleton
3. Ensure services are registered in same DI container
### Memory Growing
1. Check `MaxPendingMessages` limit
2. Verify consumers are processing messages
3. Monitor for message timeout (messages queued too long)
### Order Not Preserved
1. Set `PreserveMessageOrder: true`
2. Ensure single consumer per endpoint
3. Don't use parallel processing in handlers
## See Also
- [TCP Transport](./tcp.md) - For multi-process development
- [Transport Overview](./README.md)
- [Testing Guide](../../19_TEST_SUITE_OVERVIEW.md)

View File

@@ -0,0 +1,241 @@
# RabbitMQ Transport
The RabbitMQ transport provides durable, asynchronous message delivery using AMQP 0.9.1 protocol. Ideal for high-volume async processing, fan-out patterns, and scenarios requiring message persistence.
## Overview
| Property | Value |
|----------|-------|
| Plugin Assembly | `StellaOps.Router.Transport.RabbitMq.dll` |
| Transport Name | `rabbitmq` |
| Protocol | AMQP 0.9.1 (RabbitMQ.Client 7.x) |
| Security | TLS, SASL authentication |
| Use Case | Async processing, fan-out, durable messaging |
## Configuration
### router.yaml
```yaml
Router:
Transport:
Type: rabbitmq
RabbitMq:
HostName: rabbitmq.internal
Port: 5672
VirtualHost: /stellaops
UserName: stellaops
Password: ${RABBITMQ_PASSWORD:-}
Ssl:
Enabled: true
ServerName: rabbitmq.internal
CertPath: /certs/client.pfx
Exchange:
Name: stellaops.router
Type: topic
Durable: true
Queue:
Durable: true
AutoDelete: false
PrefetchCount: 100
```
### microservice.yaml
```yaml
routers:
- host: rabbitmq.internal
port: 5672
transportType: RabbitMq
priority: 1
rabbitmq:
virtualHost: /stellaops
exchange: stellaops.router
routingKeyPrefix: orders
```
### Environment Variables
```bash
ROUTER__TRANSPORT__TYPE=rabbitmq
ROUTER__TRANSPORT__RABBITMQ__HOSTNAME=rabbitmq.internal
ROUTER__TRANSPORT__RABBITMQ__PORT=5672
ROUTER__TRANSPORT__RABBITMQ__USERNAME=stellaops
ROUTER__TRANSPORT__RABBITMQ__PASSWORD=secret
```
## Options Reference
### Connection Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `HostName` | string | `localhost` | RabbitMQ server hostname |
| `Port` | int | `5672` | AMQP port (5671 for TLS) |
| `VirtualHost` | string | `/` | RabbitMQ virtual host |
| `UserName` | string | `guest` | Authentication username |
| `Password` | string | `guest` | Authentication password |
| `ConnectionTimeout` | TimeSpan | `00:00:30` | Connection timeout |
| `RequestedHeartbeat` | TimeSpan | `00:01:00` | Heartbeat interval |
### SSL Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `Ssl:Enabled` | bool | `false` | Enable TLS |
| `Ssl:ServerName` | string | - | Expected server certificate name |
| `Ssl:CertPath` | string | - | Client certificate path |
| `Ssl:CertPassphrase` | string | - | Client certificate password |
| `Ssl:Version` | SslProtocols | `Tls13` | TLS version |
### Exchange Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `Exchange:Name` | string | `stellaops.router` | Exchange name |
| `Exchange:Type` | string | `topic` | Exchange type (direct, topic, fanout, headers) |
| `Exchange:Durable` | bool | `true` | Survive broker restart |
| `Exchange:AutoDelete` | bool | `false` | Delete when unused |
### Queue Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `Queue:Durable` | bool | `true` | Persist messages to disk |
| `Queue:AutoDelete` | bool | `false` | Delete when all consumers disconnect |
| `Queue:Exclusive` | bool | `false` | Single-consumer queue |
| `Queue:PrefetchCount` | int | `100` | Prefetch limit per consumer |
## Message Flow
```
┌───────────────┐ ┌─────────────────────────────────────────────┐
│ Gateway │ │ RabbitMQ Broker │
│ │ │ │
│ ─────────────►│ AMQP │ ┌───────────────┐ ┌──────────────────┐ │
│ Publish │────────►│ │ Exchange │───►│ Service Queue │ │
│ │ │ │ (topic/fanout)│ │ (orders.*) │ │
└───────────────┘ │ └───────────────┘ └────────┬─────────┘ │
│ │ │
└─────────────────────────────────│──────────┘
┌───────────────────────────────────────────┐
│ Microservices │
│ ┌─────────────┐ ┌─────────────────────┐ │
│ │ OrderCreate │ │ OrderNotification │ │
│ │ Consumer │ │ Consumer │ │
│ └─────────────┘ └─────────────────────┘ │
└───────────────────────────────────────────┘
```
## Routing Patterns
### Topic Routing
```yaml
Exchange:
Type: topic
# Microservice A binds to: orders.create.#
# Microservice B binds to: orders.*.notify
# Gateway publishes to: orders.create.premium → matches A only
# Gateway publishes to: orders.cancel.notify → matches B only
```
### Fan-Out Pattern
```yaml
Exchange:
Type: fanout
# All bound queues receive every message
# Good for broadcasting events to all services
```
### Direct Routing
```yaml
Exchange:
Type: direct
# Exact routing key match required
# Gateway publishes to: order-service
# Only queue bound with key "order-service" receives
```
## Performance Characteristics
| Metric | Typical Value |
|--------|---------------|
| Latency (p50) | < 5ms |
| Latency (p99) | < 20ms |
| Throughput | 50,000+ mps |
| Memory per connection | ~16KB |
*Persistent messages with acknowledgments on dedicated broker*
## High Availability
### Clustered RabbitMQ
```yaml
RabbitMq:
Endpoints:
- Host: rabbit1.internal
Port: 5672
- Host: rabbit2.internal
Port: 5672
- Host: rabbit3.internal
Port: 5672
Queue:
Arguments:
x-ha-policy: all # Mirror to all nodes
x-queue-type: quorum # Quorum queue (RabbitMQ 3.8+)
```
### Dead Letter Handling
```yaml
Queue:
Arguments:
x-dead-letter-exchange: stellaops.dlx
x-dead-letter-routing-key: failed
x-message-ttl: 3600000 # 1 hour TTL
```
## Troubleshooting
### Connection Refused
```
Error: Failed to connect to rabbitmq.internal:5672
```
1. Verify RabbitMQ is running: `rabbitmqctl status`
2. Check firewall allows AMQP port
3. Verify virtual host exists: `rabbitmqctl list_vhosts`
4. Confirm user has permissions: `rabbitmqctl list_user_permissions stellaops`
### Authentication Failed
```
Error: ACCESS_REFUSED - Login was refused
```
1. Check username/password are correct
2. Verify user exists: `rabbitmqctl list_users`
3. Grant permissions: `rabbitmqctl set_permissions -p /stellaops stellaops ".*" ".*" ".*"`
### Messages Not Being Consumed
1. Check queue exists: `rabbitmqctl list_queues`
2. Verify binding: `rabbitmqctl list_bindings -p /stellaops`
3. Check consumer is connected: `rabbitmqctl list_consumers`
4. Monitor unacked messages: `rabbitmqctl list_queues messages_unacknowledged`
## See Also
- [RabbitMQ Documentation](https://www.rabbitmq.com/documentation.html)
- [Transport Overview](./README.md)
- [Messaging Transports](../../messaging/)

View File

@@ -0,0 +1,135 @@
# TCP Transport
The TCP transport provides high-performance binary communication for internal microservices within the same datacenter or trusted network.
## Overview
| Property | Value |
|----------|-------|
| Plugin Assembly | `StellaOps.Router.Transport.Tcp.dll` |
| Transport Name | `tcp` |
| Default Port | 5100 |
| Security | Network isolation (no encryption) |
| Use Case | Internal services, low-latency communication |
## Configuration
### router.yaml
```yaml
Router:
Transport:
Type: tcp
Tcp:
Host: "0.0.0.0"
Port: 5100
MaxConnections: 1000
ReceiveBufferSize: 65536
SendBufferSize: 65536
KeepAlive: true
NoDelay: true
```
### microservice.yaml
```yaml
routers:
- host: gateway.internal
port: 5100
transportType: Tcp
priority: 1
```
### Environment Variables
```bash
ROUTER__TRANSPORT__TYPE=tcp
ROUTER__TRANSPORT__TCP__HOST=0.0.0.0
ROUTER__TRANSPORT__TCP__PORT=5100
ROUTER__TRANSPORT__TCP__MAXCONNECTIONS=1000
```
## Options Reference
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `Host` | string | `0.0.0.0` | Bind address for server |
| `Port` | int | `5100` | TCP port number |
| `MaxConnections` | int | `1000` | Maximum concurrent connections |
| `ReceiveBufferSize` | int | `65536` | Socket receive buffer size in bytes |
| `SendBufferSize` | int | `65536` | Socket send buffer size in bytes |
| `KeepAlive` | bool | `true` | Enable TCP keep-alive probes |
| `NoDelay` | bool | `true` | Disable Nagle's algorithm (lower latency) |
| `ConnectTimeout` | TimeSpan | `00:00:30` | Connection timeout |
| `ReadTimeout` | TimeSpan | `00:02:00` | Socket read timeout |
| `WriteTimeout` | TimeSpan | `00:02:00` | Socket write timeout |
## Performance Characteristics
| Metric | Typical Value |
|--------|---------------|
| Latency (p50) | < 1ms |
| Latency (p99) | < 5ms |
| Throughput | 100,000+ rps |
| Memory per connection | ~2KB |
*Benchmarks on 10Gbps network with small payloads (<1KB)*
## Security Considerations
TCP transport does **not** provide encryption. Use only in:
- Private networks with proper network segmentation
- Same-datacenter deployments with firewalled traffic
- Container orchestration networks (Kubernetes pod network)
For encrypted communication, use [TLS transport](./tls.md).
## Framing Protocol
The TCP transport uses the standard Router binary framing protocol:
```
┌────────────────────────────────────────────────────────────────┐
│ Frame Header (24 bytes) │
├────────────┬────────────┬────────────┬────────────┬────────────┤
│ Magic (4) │ Version(2) │ Type (2) │ Flags (4) │ Length (8) │
├────────────┴────────────┴────────────┴────────────┴────────────┤
│ Correlation ID (4) │
├─────────────────────────────────────────────────────────────────┤
│ Frame Payload (variable) │
└─────────────────────────────────────────────────────────────────┘
```
## Troubleshooting
### Connection Refused
```
Error: Connection refused to gateway.internal:5100
```
1. Verify Gateway is running and listening on port 5100
2. Check firewall rules allow traffic on port 5100
3. Verify DNS resolution of hostname
### Connection Timeout
```
Error: Connection to gateway.internal:5100 timed out
```
1. Increase `ConnectTimeout` value
2. Check network connectivity between services
3. Verify no network segmentation blocking traffic
### Performance Issues
1. Enable `NoDelay: true` for latency-sensitive workloads
2. Tune buffer sizes based on payload sizes
3. Monitor connection pool exhaustion
## See Also
- [TLS Transport](./tls.md) - Encrypted variant
- [Transport Overview](./README.md)
- [Router Architecture](../ARCHITECTURE.md)

View File

@@ -0,0 +1,223 @@
# TLS Transport
The TLS transport provides encrypted communication with optional mutual TLS (mTLS) authentication for secure cross-datacenter and external service communication.
## Overview
| Property | Value |
|----------|-------|
| Plugin Assembly | `StellaOps.Router.Transport.Tls.dll` |
| Transport Name | `tls` |
| Default Port | 5101 |
| Security | TLS 1.3, optional mTLS |
| Use Case | Cross-datacenter, external services, compliance-required environments |
## Configuration
### router.yaml (Gateway/Server)
```yaml
Router:
Transport:
Type: tls
Tls:
Host: "0.0.0.0"
Port: 5101
CertificatePath: /certs/server.pfx
CertificatePassword: ${TLS_CERT_PASSWORD:-}
RequireClientCertificate: true # Enable mTLS
AllowedClientCertificates:
- /certs/trusted/client1.cer
- /certs/trusted/client2.cer
TlsProtocols: Tls13 # TLS 1.3 only
CheckCertificateRevocation: true
```
### microservice.yaml (Client)
```yaml
routers:
- host: gateway.external.company.com
port: 5101
transportType: Tls
priority: 1
tls:
clientCertificatePath: /certs/client.pfx
clientCertificatePassword: ${CLIENT_CERT_PASSWORD:-}
validateServerCertificate: true
serverCertificateThumbprints:
- "A1B2C3D4E5F6..."
```
### Environment Variables
```bash
ROUTER__TRANSPORT__TYPE=tls
ROUTER__TRANSPORT__TLS__PORT=5101
ROUTER__TRANSPORT__TLS__CERTIFICATEPATH=/certs/server.pfx
ROUTER__TRANSPORT__TLS__CERTIFICATEPASSWORD=secret
ROUTER__TRANSPORT__TLS__REQUIRECLIENTCERTIFICATE=true
```
## Options Reference
### Server Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `Host` | string | `0.0.0.0` | Bind address |
| `Port` | int | `5101` | TLS port number |
| `CertificatePath` | string | - | Path to server certificate (PFX/P12) |
| `CertificatePassword` | string | - | Password for certificate file |
| `RequireClientCertificate` | bool | `false` | Enable mutual TLS |
| `AllowedClientCertificates` | string[] | - | Paths to trusted client certs |
| `TlsProtocols` | TlsProtocols | `Tls12,Tls13` | Allowed TLS versions |
| `CheckCertificateRevocation` | bool | `true` | Check CRL/OCSP |
| `CipherSuites` | string[] | - | Allowed cipher suites (TLS 1.3) |
### Client Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `ClientCertificatePath` | string | - | Path to client certificate (PFX/P12) |
| `ClientCertificatePassword` | string | - | Password for client certificate |
| `ValidateServerCertificate` | bool | `true` | Validate server certificate |
| `ServerCertificateThumbprints` | string[] | - | Pinned server cert thumbprints |
| `AllowUntrustedCertificates` | bool | `false` | Allow self-signed certs (dev only) |
## Mutual TLS (mTLS)
For zero-trust environments, enable mutual TLS authentication:
```
┌──────────────────┐ ┌──────────────────┐
│ Microservice │ │ Gateway │
│ │ │ │
│ Client Cert │◄───── TLS ────────►│ Server Cert │
│ (identity) │ Handshake │ (identity) │
│ │ │ │
│ Validates: │ │ Validates: │
│ - Server cert │ │ - Client cert │
│ - Thumbprint │ │ - Allowlist │
└──────────────────┘ └──────────────────┘
```
### Certificate Requirements
**Server Certificate:**
- Extended Key Usage: `Server Authentication (1.3.6.1.5.5.7.3.1)`
- Subject Alternative Name: Include all DNS names clients will connect to
**Client Certificate:**
- Extended Key Usage: `Client Authentication (1.3.6.1.5.5.7.3.2)`
- Common Name or SAN identifying the service
## Performance Characteristics
| Metric | Typical Value |
|--------|---------------|
| Latency (p50) | < 2ms |
| Latency (p99) | < 10ms |
| Throughput | 80,000+ rps |
| Memory per connection | ~8KB |
*TLS 1.3 with session resumption on 10Gbps network*
## Certificate Management
### Generating Certificates
```bash
# Generate CA
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
-subj "/CN=StellaOps Internal CA"
# Generate server certificate
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr \
-subj "/CN=gateway.internal" \
-addext "subjectAltName=DNS:gateway.internal,DNS:localhost"
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt \
-extfile <(echo "subjectAltName=DNS:gateway.internal,DNS:localhost")
# Package as PFX
openssl pkcs12 -export -out server.pfx -inkey server.key -in server.crt \
-certfile ca.crt -passout pass:changeit
```
### Certificate Rotation
1. Generate new certificate before expiry
2. Update `CertificatePath` in configuration
3. Restart Gateway (no connection interruption with graceful shutdown)
4. Update client thumbprint pins if using certificate pinning
## Air-Gap Deployment
For offline environments:
1. Pre-provision all certificates
2. Disable CRL/OCSP checks: `CheckCertificateRevocation: false`
3. Use certificate pinning instead of chain validation
```yaml
Router:
Transport:
Type: tls
Tls:
CheckCertificateRevocation: false
AllowedClientCertificates:
- /certs/trusted/client1.cer
```
## Troubleshooting
### Certificate Validation Failed
```
Error: The remote certificate is invalid according to the validation procedure
```
1. Verify certificate is not expired: `openssl x509 -in cert.pem -noout -dates`
2. Check certificate chain is complete
3. Verify CA is trusted by the system or explicitly configured
### mTLS Handshake Failed
```
Error: The client certificate is not provided
```
1. Ensure client certificate is configured with correct path
2. Verify certificate has Client Authentication EKU
3. Check certificate is in Gateway's allowlist
### TLS Protocol Mismatch
```
Error: A call to SSPI failed, TLS version mismatch
```
1. Ensure both sides support compatible TLS versions
2. Update `TlsProtocols` to include common version
3. TLS 1.3 recommended for new deployments
## Compliance
The TLS transport supports compliance requirements:
| Standard | Configuration |
|----------|---------------|
| PCI-DSS | TLS 1.2+, strong ciphers, certificate validation |
| HIPAA | TLS 1.2+, mTLS for service-to-service |
| FedRAMP | TLS 1.3, FIPS-validated crypto modules |
For FIPS mode, ensure .NET is configured for FIPS compliance and use FIPS-approved cipher suites.
## See Also
- [TCP Transport](./tcp.md) - Unencrypted variant for internal use
- [Transport Overview](./README.md)
- [Security Hardening Guide](../../SECURITY_HARDENING_GUIDE.md)

View File

@@ -0,0 +1,173 @@
# UDP Transport
The UDP transport provides connectionless, fire-and-forget messaging suitable for broadcast notifications and scenarios where delivery guarantees are not critical.
## Overview
| Property | Value |
|----------|-------|
| Plugin Assembly | `StellaOps.Router.Transport.Udp.dll` |
| Transport Name | `udp` |
| Default Port | 5102 |
| Security | Network isolation (no encryption) |
| Use Case | Broadcast, metrics, fire-and-forget events |
## Configuration
### router.yaml
```yaml
Router:
Transport:
Type: udp
Udp:
Host: "0.0.0.0"
Port: 5102
ReceiveBufferSize: 65536
SendBufferSize: 65536
MulticastGroup: null # Optional multicast group
MulticastTtl: 1
EnableBroadcast: false
```
### microservice.yaml
```yaml
routers:
- host: gateway.internal
port: 5102
transportType: Udp
priority: 1
```
### Environment Variables
```bash
ROUTER__TRANSPORT__TYPE=udp
ROUTER__TRANSPORT__UDP__HOST=0.0.0.0
ROUTER__TRANSPORT__UDP__PORT=5102
```
## Options Reference
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `Host` | string | `0.0.0.0` | Bind address |
| `Port` | int | `5102` | UDP port number |
| `ReceiveBufferSize` | int | `65536` | Socket receive buffer size |
| `SendBufferSize` | int | `65536` | Socket send buffer size |
| `MulticastGroup` | string | `null` | Multicast group address (e.g., `239.1.2.3`) |
| `MulticastTtl` | int | `1` | Multicast time-to-live |
| `EnableBroadcast` | bool | `false` | Allow broadcast to `255.255.255.255` |
| `MaxDatagramSize` | int | `65507` | Maximum UDP datagram size |
## Use Cases
### Fire-and-Forget Events
For events where acknowledgment is not required:
```csharp
[StellaEndpoint("POST", "/events/metrics", FireAndForget = true)]
public sealed class MetricsEndpoint : IStellaEndpoint<MetricsBatch, EmptyResponse>
{
public Task<EmptyResponse> HandleAsync(MetricsBatch request, CancellationToken ct)
{
// Process metrics - no response expected
return Task.FromResult(new EmptyResponse());
}
}
```
### Multicast Broadcasting
For broadcasting to multiple listeners:
```yaml
Router:
Transport:
Type: udp
Udp:
MulticastGroup: "239.0.1.1"
MulticastTtl: 2
```
Services join the multicast group to receive broadcasts:
```csharp
// All services with this config receive broadcasts
routers:
- host: "239.0.1.1"
port: 5102
transportType: Udp
```
## Performance Characteristics
| Metric | Typical Value |
|--------|---------------|
| Latency (p50) | < 0.5ms |
| Latency (p99) | < 2ms |
| Throughput | 150,000+ pps |
| Memory per socket | ~1KB |
*Note: No delivery guarantees; packets may be lost under load*
## Limitations
1. **No delivery guarantees**: Packets may be dropped
2. **No ordering guarantees**: Packets may arrive out of order
3. **Size limits**: Maximum datagram size ~65KB (64KB practical limit)
4. **No acknowledgments**: Sender doesn't know if message was received
5. **No fragmentation handling**: Large messages must fit in single datagram
## When to Use UDP
**Good fit:**
- Real-time metrics and telemetry
- Service discovery announcements
- Health check broadcasts
- Low-latency gaming or streaming metadata
**Not recommended:**
- Transaction processing
- Data that must not be lost
- Large payloads (use TCP/TLS instead)
## Security Considerations
UDP transport does **not** provide encryption or authentication. Consider:
- Network segmentation
- Firewall rules to restrict UDP traffic
- Application-level message signing if integrity is needed
- DTLS wrapper for encrypted UDP (not built-in)
## Troubleshooting
### Messages Not Received
1. Check firewall allows UDP traffic on the configured port
2. Verify receiver is bound to correct address/port
3. Check for buffer overflow (increase `ReceiveBufferSize`)
4. Monitor for packet loss with network tools
### Multicast Not Working
1. Verify multicast routing is enabled on network
2. Check `MulticastTtl` is sufficient for network topology
3. Ensure IGMP snooping is properly configured
4. Verify all receivers joined the multicast group
### High Packet Loss
1. Reduce message rate
2. Increase buffer sizes
3. Check for network congestion
4. Consider switching to TCP for reliable delivery
## See Also
- [TCP Transport](./tcp.md) - Reliable delivery
- [Transport Overview](./README.md)
- [Router Architecture](../ARCHITECTURE.md)