Goal for this step: have a **concrete, runnable example** (gateway + one microservice) and a **clear skeleton** for migrating any existing `StellaOps.*.WebService` into `StellaOps.*.Microservice`. After this, devs should be able to: * Run a full vertical slice locally. * Open a “migration cookbook” and follow a predictable recipe. I’ll split it into two tracks: reference example, then migration skeleton. --- ## 1. Reference example: “Billing” vertical slice ### 1.1 Create the sample microservice project **Project:** `src/StellaOps.Billing.Microservice` **Owner:** feature/example dev Tasks: 1. Create the project: ```bash cd src dotnet new worker -n StellaOps.Billing.Microservice ``` 2. Add references: ```bash dotnet add StellaOps.Billing.Microservice/StellaOps.Billing.Microservice.csproj reference \ __Libraries/StellaOps.Microservice/StellaOps.Microservice.csproj dotnet add StellaOps.Billing.Microservice/StellaOps.Billing.Microservice.csproj reference \ __Libraries/StellaOps.Router.Common/StellaOps.Router.Common.csproj ``` 3. In `Program.cs`, wire the SDK with **InMemory transport** for now: ```csharp var builder = Host.CreateApplicationBuilder(args); builder.Services.AddStellaMicroservice(opts => { opts.ServiceName = "Billing"; opts.Version = "1.0.0"; opts.Region = "eu1"; opts.InstanceId = $"billing-{Environment.MachineName}"; opts.Routers.Add(new RouterEndpointConfig { Host = "localhost", Port = 50050, // to match gateway’s InMemory/TCP harness TransportType = TransportType.Tcp }); opts.ConfigFilePath = "billing.microservice.yaml"; // optional overrides }); var app = builder.Build(); await app.RunAsync(); ``` (You can keep `TransportType` as TCP even if implemented in-process for now; once real TCP is in, nothing changes here.) --- ### 1.2 Implement a few canonical endpoints Pick 3–4 endpoints that exercise different features: 1. **Health / contract check** ```csharp [StellaEndpoint("GET", "/ping")] public sealed class PingEndpoint : IRawStellaEndpoint { public Task HandleAsync(RawRequestContext ctx) { var resp = new RawResponse { StatusCode = 200 }; resp.Headers["Content-Type"] = "text/plain"; resp.WriteBodyAsync = async stream => { await stream.WriteAsync("pong"u8.ToArray(), ctx.CancellationToken); }; return Task.FromResult(resp); } } ``` 2. **Simple JSON read/write (non-streaming)** ```csharp public sealed record CreateInvoiceRequest(string CustomerId, decimal Amount); public sealed record CreateInvoiceResponse(Guid Id); [StellaEndpoint("POST", "/billing/invoices")] public sealed class CreateInvoiceEndpoint : IStellaEndpoint { public Task HandleAsync(CreateInvoiceRequest req, CancellationToken ct) { // pretend to store in DB return Task.FromResult(new CreateInvoiceResponse(Guid.NewGuid())); } } ``` 3. **Streaming upload (large file)** ```csharp [StellaEndpoint("POST", "/billing/invoices/upload")] public sealed class InvoiceUploadEndpoint : IRawStellaEndpoint { public async Task HandleAsync(RawRequestContext ctx) { var buffer = new byte[64 * 1024]; var total = 0L; int read; while ((read = await ctx.Body.ReadAsync(buffer.AsMemory(0, buffer.Length), ctx.CancellationToken)) > 0) { total += read; // process chunk or write to temp file } var resp = new RawResponse { StatusCode = 200 }; resp.Headers["Content-Type"] = "application/json"; resp.WriteBodyAsync = async stream => { var json = $"{{\"bytesReceived\":{total}}}"; await stream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(json), ctx.CancellationToken); }; return resp; } } ``` This gives devs examples of: * Raw endpoint (`/ping`, `/upload`). * Typed endpoint (`/billing/invoices`). * Streaming usage (`Body.ReadAsync`). --- ### 1.3 Microservice YAML override example **File:** `src/StellaOps.Billing.Microservice/billing.microservice.yaml` ```yaml endpoints: - method: GET path: /ping timeout: 00:00:02 - method: POST path: /billing/invoices timeout: 00:00:05 supportsStreaming: false requiringClaims: - type: role value: BillingWriter - method: POST path: /billing/invoices/upload timeout: 00:02:00 supportsStreaming: true requiringClaims: - type: role value: BillingUploader ``` This file demonstrates: * Timeout override. * Streaming flag. * `RequiringClaims` usage. --- ### 1.4 Gateway example config for Billing **File:** `config/router.billing.yaml` (for local dev) ```yaml nodeId: "gw-dev-01" region: "eu1" payloadLimits: maxRequestBytesPerCall: 10485760 # 10 MB maxRequestBytesPerConnection: 52428800 # 50 MB maxAggregateInflightBytes: 209715200 # 200 MB services: - name: "Billing" defaultVersion: "1.0.0" endpoints: - method: "GET" path: "/ping" # router defaults, if any - method: "POST" path: "/billing/invoices" defaultTimeout: "00:00:05" requiringClaims: - type: "role" value: "BillingWriter" - method: "POST" path: "/billing/invoices/upload" defaultTimeout: "00:02:00" supportsStreaming: true requiringClaims: - type: "role" value: "BillingUploader" ``` This lets you show precedence: * Reflection → microservice YAML → router YAML. --- ### 1.5 Gateway wiring for the example **Project:** `StellaOps.Gateway.WebService` In `Program.cs`: 1. Load router config and point it to `router.billing.yaml` for dev: ```csharp builder.Configuration .AddJsonFile("appsettings.json", optional: true) .AddEnvironmentVariables(prefix: "STELLAOPS_"); builder.Services.AddOptions() .Configure((cfg, configuration) => { configuration.GetSection("Router").Bind(cfg); var yamlPath = configuration["Router:YamlPath"] ?? "config/router.billing.yaml"; if (File.Exists(yamlPath)) { var yamlCfg = RouterConfigLoader.LoadFromFile(yamlPath); // either cfg = yamlCfg (if you treat YAML as source of truth) OverlayRouterConfig(cfg, yamlCfg); } }); builder.Services.AddOptions() .Configure>((node, routerCfg) => { var cfg = routerCfg.Value; node.NodeId = cfg.NodeId; node.Region = cfg.Region; }); ``` 2. Ensure you start the appropriate transport server (for dev, TCP on localhost:50050): * From `RouterConfig.Transports` or a dev shortcut, start the TCP server listening on that port. 3. HTTP pipeline: * `EndpointResolutionMiddleware` * `RoutingDecisionMiddleware` * `TransportDispatchMiddleware` Now your dev loop is: * Run `StellaOps.Gateway.WebService`. * Run `StellaOps.Billing.Microservice`. * `curl http://localhost:{gatewayPort}/ping` → should go through gateway to microservice and back. * Similarly for `/billing/invoices` and `/billing/invoices/upload`. --- ### 1.6 Example documentation Create `docs/router/examples/Billing.Sample.md`: * “How to run the example”: * build solution * `dotnet run` for gateway * `dotnet run` for Billing microservice * Show sample `curl` commands: * `curl http://localhost:8080/ping` * `curl -X POST http://localhost:8080/billing/invoices -d '{"customerId":"C1","amount":123.45}'` * `curl -X POST http://localhost:8080/billing/invoices/upload --data-binary @bigfile.bin` * Note where config files live and how to change them. This becomes your canonical reference for new teams. --- ## 2. Migration skeleton: from WebService to Microservice Now that you have a working example, you need a **repeatable recipe** for migrating any existing `StellaOps.*.WebService` into the microservice router model. ### 2.1 Define the migration target shape For each webservice you migrate, you want: * A new project: `StellaOps.{Domain}.Microservice`. * Shared domain logic extracted into a library (if not already): `StellaOps.{Domain}.Core` or similar. * Controllers → endpoint classes: * `Controller` methods ⇨ `[StellaEndpoint]`-annotated types. * `HttpGet/HttpPost` attributes ⇨ `Method` and `Path` pair. * Configuration: * WebService’s appsettings routes → microservice YAML + router YAML. * Authentication/authorization → `RequiringClaims` in endpoint metadata. Document this target shape in `docs/router/Migration of Webservices to Microservices.md`. --- ### 2.2 Skeleton microservice template Create a **generic** microservice skeleton that any team can copy: **Project:** `templates/StellaOps.Template.Microservice` or at least a folder `samples/MigrationSkeleton/`. Contents: * `Program.cs`: ```csharp var builder = Host.CreateApplicationBuilder(args); builder.Services.AddStellaMicroservice(opts => { opts.ServiceName = "{DomainName}"; opts.Version = "1.0.0"; opts.Region = "eu1"; opts.InstanceId = "{DomainName}-" + Environment.MachineName; // Mandatory router pool configuration opts.Routers.Add(new RouterEndpointConfig { Host = "localhost", // or injected via env Port = 50050, TransportType = TransportType.Tcp }); opts.ConfigFilePath = $"{DomainName}.microservice.yaml"; }); // domain DI (reuse existing domain services from WebService) // builder.Services.AddDomainServices(); var app = builder.Build(); await app.RunAsync(); ``` * A sample endpoint mapping from a typical WebService controller method: Legacy controller: ```csharp [ApiController] [Route("api/billing/invoices")] public class InvoicesController : ControllerBase { [HttpPost] [Authorize(Roles = "BillingWriter")] public async Task> Create(CreateInvoiceRequest request) { var result = await _service.Create(request); return Ok(result); } } ``` Microservice endpoint: ```csharp [StellaEndpoint("POST", "/billing/invoices")] public sealed class CreateInvoiceEndpoint : IStellaEndpoint { private readonly IInvoiceService _service; public CreateInvoiceEndpoint(IInvoiceService service) { _service = service; } public Task HandleAsync(CreateInvoiceRequest request, CancellationToken ct) { return _service.Create(request, ct); } } ``` And matching YAML: ```yaml endpoints: - method: POST path: /billing/invoices timeout: 00:00:05 requiringClaims: - type: role value: BillingWriter ``` This skeleton demonstrates the mapping clearly. --- ### 2.3 Migration workflow for a team (per service) Put this as a checklist in `Migration of Webservices to Microservices.md`: 1. **Inventory existing HTTP surface** * List all controllers and actions with: * HTTP method. * Route template (full path). * Auth attributes (`[Authorize(Roles=..)]` or policies). * Whether the action handles large uploads/downloads. 2. **Create microservice project** * Add `StellaOps.{Domain}.Microservice` using the skeleton. * Reference domain logic project (`StellaOps.{Domain}.Core`), or extract one if necessary. 3. **Map each controller action → endpoint** For each action: * Create an endpoint class in the microservice: * `IRawStellaEndpoint` for: * Large payloads. * Very custom body handling. * `IStellaEndpoint` for standard JSON APIs. * Use `[StellaEndpoint("{METHOD}", "{PATH}")]` matching the existing route. 4. **Wire domain services & auth** * Register the same domain services the WebService used (DB contexts, repositories, etc.). * Translate role/claim-based `[Authorize]` usage to microservice YAML `RequiringClaims`. 5. **Create microservice YAML** * For each new endpoint: * Define default timeout. * `supportsStreaming: true` where appropriate. * `requiringClaims` matching prior auth requirements. 6. **Update router YAML** * Add service entry under `services`: * `name: "{Domain}"`. * `defaultVersion: "1.0.0"`. * Add endpoints (method/path, router-side overrides if needed). 7. **Smoke-test locally** * Run gateway + microservice side-by-side. * Hit the same URLs via gateway that previously were served by the WebService directly. * Compare behavior (status codes, semantics) with existing environment. 8. **Gradual rollout** Strategy options: * **Proxy mode**: * Keep WebService behind gateway for a while. * Add router endpoints that proxy to existing WebService (via HTTP) while microservice matures. * Gradually switch endpoints to microservice once stable. * **Blue/green**: * Run WebService and Microservice in parallel. * Route a small percentage of traffic to microservice via router. * Increase gradually. Outline these as patterns in the migration doc, but keep them high-level here. --- ### 2.4 Migration skeleton repository structure Add a clear place in repo for skeleton code & docs: ```text /docs /router Migration of Webservices to Microservices.md examples/ Billing.Sample.md /samples /Billing StellaOps.Billing.Microservice/ # full example project router.billing.yaml # example router config /MigrationSkeleton StellaOps.Template.Microservice/ # template project example-controller-mapping.md # before/after snippet ``` The **skeleton** project should: * Compile. * Contain TODO markers where teams fill in domain pieces. * Be referenced in the migration doc so people know where to look. --- ### 2.5 Tests to make the reference stick Add a minimal test suite around the Billing example: * **Integration tests** in `tests/StellaOps.Billing.IntegrationTests`: * Start gateway + Billing microservice (using in-memory test host or docker-compose). * `GET /ping` returns 200 and “pong”. * `POST /billing/invoices` returns 200 with a JSON body containing an `id`. * `POST /billing/invoices/upload` with a large payload succeeds and reports `bytesReceived`. * Use these tests as a reference for future services: they show how to spin up a microservice + gateway in tests. --- ## 3. Done criteria for step 11 You can treat “Build a reference example + migration skeleton” as complete when: * `StellaOps.Billing.Microservice` exists, runs, and successfully serves requests through the gateway using your real transport (or InMemory/TCP for dev). * `router.billing.yaml` plus `billing.microservice.yaml` show config patterns for: * timeouts * streaming * requiringClaims * `docs/router/examples/Billing.Sample.md` explains how to run and test the example. * `Migration of Webservices to Microservices.md` contains: * A concrete mapping example (controller → endpoint + YAML). * A step-by-step migration checklist for teams. * Pointers to the skeleton project and sample configs. * A template microservice project exists (`StellaOps.Template.Microservice` or equivalent) that teams can copy to bootstrap new services. Once you have this, onboarding new domains and migrating old WebServices stops being an ad-hoc effort and becomes a repeatable, documented process.