Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implemented comprehensive unit tests for RabbitMqTransportServer, covering constructor, disposal, connection management, event handlers, and exception handling. - Added configuration tests for RabbitMqTransportServer to validate SSL, durable queues, auto-recovery, and custom virtual host options. - Created unit tests for UdpFrameProtocol, including frame parsing and serialization, header size validation, and round-trip data preservation. - Developed tests for UdpTransportClient, focusing on connection handling, event subscriptions, and exception scenarios. - Established tests for UdpTransportServer, ensuring proper start/stop behavior, connection state management, and event handling. - Included tests for UdpTransportOptions to verify default values and modification capabilities. - Enhanced service registration tests for Udp transport services in the dependency injection container.
463 lines
15 KiB
Markdown
463 lines
15 KiB
Markdown
# StellaOps Router Migration Guide
|
|
|
|
This guide describes how to migrate existing `StellaOps.*.WebService` projects to the new microservice pattern with the StellaOps Router.
|
|
|
|
## Overview
|
|
|
|
The router provides a transport-agnostic communication layer between services, replacing direct HTTP calls with efficient binary protocols (TCP, TLS, UDP, RabbitMQ). Benefits include:
|
|
|
|
- **Performance**: Binary framing vs HTTP overhead
|
|
- **Streaming**: First-class support for large payloads
|
|
- **Cancellation**: Propagated across service boundaries
|
|
- **Claims**: Authority-integrated authorization
|
|
- **Health**: Automatic heartbeat and failover
|
|
|
|
## Prerequisites
|
|
|
|
Before migrating, ensure:
|
|
|
|
1. Router infrastructure is deployed (Gateway, transports)
|
|
2. Authority is configured with endpoint claims
|
|
3. Local development environment has router.yaml configured
|
|
|
|
## Migration Strategies
|
|
|
|
### Strategy A: In-Place Adaptation
|
|
|
|
Best for services that need to maintain HTTP compatibility during transition.
|
|
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ StellaOps.*.WebService │
|
|
│ ┌─────────────────────────────────┐│
|
|
│ │ Existing HTTP Controllers ││◄── HTTP clients (legacy)
|
|
│ └─────────────────────────────────┘│
|
|
│ ┌─────────────────────────────────┐│
|
|
│ │ [StellaEndpoint] Handlers ││◄── Router (new)
|
|
│ └─────────────────────────────────┘│
|
|
│ ┌─────────────────────────────────┐│
|
|
│ │ Shared Domain Logic ││
|
|
│ └─────────────────────────────────┘│
|
|
└─────────────────────────────────────┘
|
|
```
|
|
|
|
**Steps:**
|
|
|
|
1. Add `StellaOps.Microservice` package reference
|
|
2. Create handler classes for each HTTP route
|
|
3. Handlers call existing service layer
|
|
4. Register with router alongside HTTP
|
|
5. Test via router
|
|
6. Shift traffic gradually
|
|
7. Remove HTTP controllers when ready
|
|
|
|
**Pros:**
|
|
- Gradual migration
|
|
- No downtime
|
|
- Can roll back easily
|
|
|
|
**Cons:**
|
|
- Dual maintenance during transition
|
|
- May delay cleanup
|
|
|
|
### Strategy B: Clean Split
|
|
|
|
Best for major refactoring or when HTTP compatibility is not needed.
|
|
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ StellaOps.*.Domain │ ◄── Shared library
|
|
│ (extracted business logic) │
|
|
└─────────────────────────────────────┘
|
|
▲ ▲
|
|
│ │
|
|
┌─────────┴───────┐ ┌───────┴─────────┐
|
|
│ (Legacy) │ │ (New) │
|
|
│ *.WebService │ │ *.Microservice │
|
|
│ HTTP only │ │ Router only │
|
|
└─────────────────┘ └─────────────────┘
|
|
```
|
|
|
|
**Steps:**
|
|
|
|
1. Extract domain logic to `.Domain` library
|
|
2. Create new `.Microservice` project
|
|
3. Implement handlers using domain library
|
|
4. Deploy alongside WebService
|
|
5. Shift traffic to router
|
|
6. Deprecate WebService
|
|
|
|
**Pros:**
|
|
- Clean architecture
|
|
- No legacy code in new project
|
|
- Clear separation of concerns
|
|
|
|
**Cons:**
|
|
- More upfront work
|
|
- Requires domain extraction
|
|
|
|
## Controller to Handler Mapping
|
|
|
|
### Before (ASP.NET Controller)
|
|
|
|
```csharp
|
|
[ApiController]
|
|
[Route("api/invoices")]
|
|
public class InvoicesController : ControllerBase
|
|
{
|
|
private readonly IInvoiceService _service;
|
|
|
|
[HttpPost]
|
|
[Authorize(Roles = "billing-admin")]
|
|
public async Task<IActionResult> Create(
|
|
[FromBody] CreateInvoiceRequest request,
|
|
CancellationToken ct)
|
|
{
|
|
var invoice = await _service.CreateAsync(request);
|
|
return Ok(new { invoice.Id });
|
|
}
|
|
|
|
[HttpGet("{id}")]
|
|
public async Task<IActionResult> Get(string id)
|
|
{
|
|
var invoice = await _service.GetAsync(id);
|
|
if (invoice == null) return NotFound();
|
|
return Ok(invoice);
|
|
}
|
|
}
|
|
```
|
|
|
|
### After (Microservice Handler)
|
|
|
|
```csharp
|
|
// Handler for POST /api/invoices
|
|
[StellaEndpoint("POST", "/api/invoices", RequiredClaims = ["invoices:write"])]
|
|
public sealed class CreateInvoiceEndpoint : IStellaEndpoint<CreateInvoiceRequest, CreateInvoiceResponse>
|
|
{
|
|
private readonly IInvoiceService _service;
|
|
|
|
public CreateInvoiceEndpoint(IInvoiceService service) => _service = service;
|
|
|
|
public async Task<CreateInvoiceResponse> HandleAsync(
|
|
CreateInvoiceRequest request,
|
|
CancellationToken ct)
|
|
{
|
|
var invoice = await _service.CreateAsync(request, ct);
|
|
return new CreateInvoiceResponse { InvoiceId = invoice.Id };
|
|
}
|
|
}
|
|
|
|
// Handler for GET /api/invoices/{id}
|
|
[StellaEndpoint("GET", "/api/invoices/{id}", RequiredClaims = ["invoices:read"])]
|
|
public sealed class GetInvoiceEndpoint : IStellaEndpoint<GetInvoiceRequest, GetInvoiceResponse>
|
|
{
|
|
private readonly IInvoiceService _service;
|
|
|
|
public GetInvoiceEndpoint(IInvoiceService service) => _service = service;
|
|
|
|
public async Task<GetInvoiceResponse> HandleAsync(
|
|
GetInvoiceRequest request,
|
|
CancellationToken ct)
|
|
{
|
|
var invoice = await _service.GetAsync(request.Id, ct);
|
|
return new GetInvoiceResponse
|
|
{
|
|
InvoiceId = invoice?.Id,
|
|
Found = invoice != null
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
## CancellationToken Wiring
|
|
|
|
**This is the #1 source of migration bugs.** Every async operation must receive and respect the cancellation token.
|
|
|
|
### Checklist
|
|
|
|
For each migrated handler, verify:
|
|
|
|
- [ ] Handler accepts CancellationToken parameter (automatic with IStellaEndpoint)
|
|
- [ ] Token passed to all database calls
|
|
- [ ] Token passed to all HTTP client calls
|
|
- [ ] Token passed to all file I/O operations
|
|
- [ ] Long-running loops check `ct.IsCancellationRequested`
|
|
- [ ] Token passed to `Task.Delay`, `WaitAsync`, etc.
|
|
|
|
### Example: Before (missing tokens)
|
|
|
|
```csharp
|
|
public async Task<Invoice> CreateAsync(CreateInvoiceRequest request)
|
|
{
|
|
var invoice = new Invoice(request);
|
|
await _db.Invoices.AddAsync(invoice); // Missing token!
|
|
await _db.SaveChangesAsync(); // Missing token!
|
|
await _notifier.SendAsync(invoice); // Missing token!
|
|
return invoice;
|
|
}
|
|
```
|
|
|
|
### Example: After (proper wiring)
|
|
|
|
```csharp
|
|
public async Task<Invoice> CreateAsync(CreateInvoiceRequest request, CancellationToken ct)
|
|
{
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
var invoice = new Invoice(request);
|
|
await _db.Invoices.AddAsync(invoice, ct);
|
|
await _db.SaveChangesAsync(ct);
|
|
await _notifier.SendAsync(invoice, ct);
|
|
return invoice;
|
|
}
|
|
```
|
|
|
|
## Streaming Migration
|
|
|
|
### File Upload: Before
|
|
|
|
```csharp
|
|
[HttpPost("upload")]
|
|
public async Task<IActionResult> Upload(IFormFile file)
|
|
{
|
|
using var stream = file.OpenReadStream();
|
|
await _storage.SaveAsync(stream);
|
|
return Ok();
|
|
}
|
|
```
|
|
|
|
### File Upload: After
|
|
|
|
```csharp
|
|
[StellaEndpoint("POST", "/upload", SupportsStreaming = true)]
|
|
public sealed class UploadEndpoint : IRawStellaEndpoint
|
|
{
|
|
private readonly IStorageService _storage;
|
|
|
|
public UploadEndpoint(IStorageService storage) => _storage = storage;
|
|
|
|
public async Task<RawResponse> HandleAsync(RawRequestContext ctx, CancellationToken ct)
|
|
{
|
|
// ctx.Body is already a stream - no buffering needed
|
|
var path = await _storage.SaveAsync(ctx.Body, ct);
|
|
return RawResponse.Ok($"{{\"path\":\"{path}\"}}");
|
|
}
|
|
}
|
|
```
|
|
|
|
### File Download: Before
|
|
|
|
```csharp
|
|
[HttpGet("download/{id}")]
|
|
public async Task<IActionResult> Download(string id)
|
|
{
|
|
var stream = await _storage.GetAsync(id);
|
|
return File(stream, "application/octet-stream");
|
|
}
|
|
```
|
|
|
|
### File Download: After
|
|
|
|
```csharp
|
|
[StellaEndpoint("GET", "/download/{id}", SupportsStreaming = true)]
|
|
public sealed class DownloadEndpoint : IRawStellaEndpoint
|
|
{
|
|
private readonly IStorageService _storage;
|
|
|
|
public DownloadEndpoint(IStorageService storage) => _storage = storage;
|
|
|
|
public async Task<RawResponse> HandleAsync(RawRequestContext ctx, CancellationToken ct)
|
|
{
|
|
var id = ctx.PathParameters["id"];
|
|
var stream = await _storage.GetAsync(id, ct);
|
|
return RawResponse.Stream(stream, "application/octet-stream");
|
|
}
|
|
}
|
|
```
|
|
|
|
## Authorization Migration
|
|
|
|
### Before: [Authorize] Attribute
|
|
|
|
```csharp
|
|
[Authorize(Roles = "admin,billing-manager")]
|
|
public async Task<IActionResult> Delete(string id) { ... }
|
|
```
|
|
|
|
### After: RequiredClaims
|
|
|
|
```csharp
|
|
[StellaEndpoint("DELETE", "/invoices/{id}", RequiredClaims = ["invoices:delete"])]
|
|
public sealed class DeleteInvoiceEndpoint : IStellaEndpoint<...> { ... }
|
|
```
|
|
|
|
Claims are configured in Authority and enforced by the Gateway's AuthorizationMiddleware.
|
|
|
|
## Migration Checklist Template
|
|
|
|
Use this checklist for each service migration:
|
|
|
|
```markdown
|
|
# Migration Checklist: [ServiceName]
|
|
|
|
## Inventory
|
|
- [ ] List all HTTP routes (Method + Path)
|
|
- [ ] Identify streaming endpoints
|
|
- [ ] Identify authorization requirements
|
|
- [ ] Document external dependencies
|
|
|
|
## Preparation
|
|
- [ ] Add StellaOps.Microservice package
|
|
- [ ] Add StellaOps.Router.Transport.* package(s)
|
|
- [ ] Configure router connection in Program.cs
|
|
- [ ] Set up local gateway for testing
|
|
|
|
## Per-Route Migration
|
|
For each route:
|
|
- [ ] Create [StellaEndpoint] handler class
|
|
- [ ] Define request/response record types
|
|
- [ ] Map path parameters
|
|
- [ ] Wire CancellationToken throughout
|
|
- [ ] Convert to IRawStellaEndpoint if streaming
|
|
- [ ] Add RequiredClaims
|
|
- [ ] Write unit tests
|
|
- [ ] Write integration tests
|
|
|
|
## Cutover
|
|
- [ ] Deploy alongside existing WebService
|
|
- [ ] Verify via router routing
|
|
- [ ] Shift percentage of traffic
|
|
- [ ] Monitor for errors
|
|
- [ ] Full cutover
|
|
- [ ] Remove WebService HTTP listeners
|
|
|
|
## Cleanup
|
|
- [ ] Remove unused controller code
|
|
- [ ] Remove HTTP pipeline configuration
|
|
- [ ] Update OpenAPI documentation
|
|
- [ ] Update client SDKs
|
|
```
|
|
|
|
## Service Inventory
|
|
|
|
| Module | WebService Project | Priority | Complexity | Notes |
|
|
|--------|-------------------|----------|------------|-------|
|
|
| Gateway | StellaOps.Gateway.WebService | N/A | N/A | IS the router |
|
|
| Concelier | StellaOps.Concelier.WebService | High | Medium | Advisory ingestion |
|
|
| Scanner | StellaOps.Scanner.WebService | High | High | Streaming scans |
|
|
| Attestor | StellaOps.Attestor.WebService | Medium | Medium | Attestation gen |
|
|
| Excititor | StellaOps.Excititor.WebService | Medium | Low | VEX processing |
|
|
| Orchestrator | StellaOps.Orchestrator.WebService | Medium | Medium | Job coordination |
|
|
| Scheduler | StellaOps.Scheduler.WebService | Low | Low | Job scheduling |
|
|
| Notify | StellaOps.Notify.WebService | Low | Low | Notifications |
|
|
| Notifier | StellaOps.Notifier.WebService | Low | Low | Alert dispatch |
|
|
| Signer | StellaOps.Signer.WebService | Medium | Low | Crypto signing |
|
|
| Findings | StellaOps.Findings.Ledger.WebService | Medium | Medium | Results storage |
|
|
| EvidenceLocker | StellaOps.EvidenceLocker.WebService | Low | Medium | Blob storage |
|
|
| ExportCenter | StellaOps.ExportCenter.WebService | Low | Medium | Report generation |
|
|
| IssuerDirectory | StellaOps.IssuerDirectory.WebService | Low | Low | Issuer lookup |
|
|
| PacksRegistry | StellaOps.PacksRegistry.WebService | Low | Low | Pack management |
|
|
| RiskEngine | StellaOps.RiskEngine.WebService | Medium | Medium | Risk calculation |
|
|
| TaskRunner | StellaOps.TaskRunner.WebService | Low | Medium | Task execution |
|
|
| TimelineIndexer | StellaOps.TimelineIndexer.WebService | Low | Low | Event indexing |
|
|
| AdvisoryAI | StellaOps.AdvisoryAI.WebService | Low | Medium | AI assistance |
|
|
|
|
## Testing During Migration
|
|
|
|
### Unit Tests
|
|
|
|
Test handlers in isolation using mocked dependencies:
|
|
|
|
```csharp
|
|
[Fact]
|
|
public async Task CreateInvoice_ValidRequest_ReturnsInvoiceId()
|
|
{
|
|
// Arrange
|
|
var mockService = new Mock<IInvoiceService>();
|
|
mockService.Setup(s => s.CreateAsync(It.IsAny<CreateInvoiceRequest>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new Invoice { Id = "INV-123" });
|
|
|
|
var endpoint = new CreateInvoiceEndpoint(mockService.Object);
|
|
|
|
// Act
|
|
var response = await endpoint.HandleAsync(
|
|
new CreateInvoiceRequest { Amount = 100 },
|
|
CancellationToken.None);
|
|
|
|
// Assert
|
|
response.InvoiceId.Should().Be("INV-123");
|
|
}
|
|
```
|
|
|
|
### Integration Tests
|
|
|
|
Use WebApplicationFactory for the Gateway and actual microservice instances:
|
|
|
|
```csharp
|
|
public sealed class InvoiceTests : IClassFixture<GatewayFixture>
|
|
{
|
|
private readonly GatewayFixture _fixture;
|
|
|
|
[Fact]
|
|
public async Task CreateAndGetInvoice_WorksEndToEnd()
|
|
{
|
|
var createResponse = await _fixture.Client.PostAsJsonAsync("/api/invoices",
|
|
new { Amount = 100 });
|
|
createResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
|
|
var created = await createResponse.Content.ReadFromJsonAsync<CreateInvoiceResponse>();
|
|
|
|
var getResponse = await _fixture.Client.GetAsync($"/api/invoices/{created.InvoiceId}");
|
|
getResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Common Migration Issues
|
|
|
|
### 1. Missing CancellationToken Propagation
|
|
|
|
**Symptom:** Requests continue processing after client disconnects.
|
|
|
|
**Fix:** Pass `CancellationToken` to all async operations.
|
|
|
|
### 2. IFormFile Not Available
|
|
|
|
**Symptom:** Compilation error on `IFormFile` parameter.
|
|
|
|
**Fix:** Convert to `IRawStellaEndpoint` for streaming.
|
|
|
|
### 3. HttpContext Not Available
|
|
|
|
**Symptom:** Code references `HttpContext` for headers, claims.
|
|
|
|
**Fix:** Use `RawRequestContext` for raw endpoints, or inject claims via Authority.
|
|
|
|
### 4. Return Type Mismatch
|
|
|
|
**Symptom:** Handler returns `IActionResult`.
|
|
|
|
**Fix:** Define proper response record type, return that instead.
|
|
|
|
### 5. Route Parameter Not Extracted
|
|
|
|
**Symptom:** Path parameters like `{id}` not populated.
|
|
|
|
**Fix:** For `IStellaEndpoint`, add property to request type. For `IRawStellaEndpoint`, use `ctx.PathParameters["id"]`.
|
|
|
|
## Next Steps
|
|
|
|
1. Choose a low-risk service for pilot migration (Scheduler recommended)
|
|
2. Follow the Migration Checklist
|
|
3. Document lessons learned
|
|
4. Proceed with higher-priority services
|
|
5. Eventually merge all to use router exclusively
|
|
|
|
---
|
|
|
|
## See Also
|
|
|
|
- [Router Architecture](architecture.md) - System specification
|
|
- [Schema Validation](schema-validation.md) - JSON Schema validation
|
|
- [OpenAPI Aggregation](openapi-aggregation.md) - OpenAPI document generation
|