- 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.
15 KiB
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:
- Router infrastructure is deployed (Gateway, transports)
- Authority is configured with endpoint claims
- 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:
- Add
StellaOps.Microservicepackage reference - Create handler classes for each HTTP route
- Handlers call existing service layer
- Register with router alongside HTTP
- Test via router
- Shift traffic gradually
- 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:
- Extract domain logic to
.Domainlibrary - Create new
.Microserviceproject - Implement handlers using domain library
- Deploy alongside WebService
- Shift traffic to router
- 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)
[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)
// 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)
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)
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
[HttpPost("upload")]
public async Task<IActionResult> Upload(IFormFile file)
{
using var stream = file.OpenReadStream();
await _storage.SaveAsync(stream);
return Ok();
}
File Upload: After
[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
[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
[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
[Authorize(Roles = "admin,billing-manager")]
public async Task<IActionResult> Delete(string id) { ... }
After: RequiredClaims
[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:
# 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:
[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:
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
- Choose a low-risk service for pilot migration (Scheduler recommended)
- Follow the Migration Checklist
- Document lessons learned
- Proceed with higher-priority services
- Eventually merge all to use router exclusively
See Also
- Router Architecture - System specification
- Schema Validation - JSON Schema validation
- OpenAPI Aggregation - OpenAPI document generation