# 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 Create( [FromBody] CreateInvoiceRequest request, CancellationToken ct) { var invoice = await _service.CreateAsync(request); return Ok(new { invoice.Id }); } [HttpGet("{id}")] public async Task 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 { private readonly IInvoiceService _service; public CreateInvoiceEndpoint(IInvoiceService service) => _service = service; public async Task 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 { private readonly IInvoiceService _service; public GetInvoiceEndpoint(IInvoiceService service) => _service = service; public async Task 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 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 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 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 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 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 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 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(); mockService.Setup(s => s.CreateAsync(It.IsAny(), It.IsAny())) .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 { 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(); 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