15 KiB
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:
- Create the project:
cd src
dotnet new worker -n StellaOps.Billing.Microservice
- Add references:
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
- In
Program.cs, wire the SDK with InMemory transport for now:
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:
- Health / contract check
[StellaEndpoint("GET", "/ping")]
public sealed class PingEndpoint : IRawStellaEndpoint
{
public Task<RawResponse> 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);
}
}
- Simple JSON read/write (non-streaming)
public sealed record CreateInvoiceRequest(string CustomerId, decimal Amount);
public sealed record CreateInvoiceResponse(Guid Id);
[StellaEndpoint("POST", "/billing/invoices")]
public sealed class CreateInvoiceEndpoint : IStellaEndpoint<CreateInvoiceRequest, CreateInvoiceResponse>
{
public Task<CreateInvoiceResponse> HandleAsync(CreateInvoiceRequest req, CancellationToken ct)
{
// pretend to store in DB
return Task.FromResult(new CreateInvoiceResponse(Guid.NewGuid()));
}
}
- Streaming upload (large file)
[StellaEndpoint("POST", "/billing/invoices/upload")]
public sealed class InvoiceUploadEndpoint : IRawStellaEndpoint
{
public async Task<RawResponse> 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
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.
RequiringClaimsusage.
1.4 Gateway example config for Billing
File: config/router.billing.yaml (for local dev)
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:
- Load router config and point it to
router.billing.yamlfor dev:
builder.Configuration
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables(prefix: "STELLAOPS_");
builder.Services.AddOptions<RouterConfig>()
.Configure<IConfiguration>((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<GatewayNodeConfig>()
.Configure<IOptions<RouterConfig>>((node, routerCfg) =>
{
var cfg = routerCfg.Value;
node.NodeId = cfg.NodeId;
node.Region = cfg.Region;
});
- Ensure you start the appropriate transport server (for dev, TCP on localhost:50050):
- From
RouterConfig.Transportsor a dev shortcut, start the TCP server listening on that port.
- HTTP pipeline:
EndpointResolutionMiddlewareRoutingDecisionMiddlewareTransportDispatchMiddleware
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/invoicesand/billing/invoices/upload.
1.6 Example documentation
Create docs/router/examples/Billing.Sample.md:
-
“How to run the example”:
- build solution
dotnet runfor gatewaydotnet runfor Billing microservice
-
Show sample
curlcommands:curl http://localhost:8080/pingcurl -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}.Coreor similar. -
Controllers → endpoint classes:
Controllermethods ⇨[StellaEndpoint]-annotated types.HttpGet/HttpPostattributes ⇨MethodandPathpair.
-
Configuration:
- WebService’s appsettings routes → microservice YAML + router YAML.
- Authentication/authorization →
RequiringClaimsin 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:
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:
[ApiController] [Route("api/billing/invoices")] public class InvoicesController : ControllerBase { [HttpPost] [Authorize(Roles = "BillingWriter")] public async Task<ActionResult<InvoiceDto>> Create(CreateInvoiceRequest request) { var result = await _service.Create(request); return Ok(result); } }Microservice endpoint:
[StellaEndpoint("POST", "/billing/invoices")] public sealed class CreateInvoiceEndpoint : IStellaEndpoint<CreateInvoiceRequest, InvoiceDto> { private readonly IInvoiceService _service; public CreateInvoiceEndpoint(IInvoiceService service) { _service = service; } public Task<InvoiceDto> HandleAsync(CreateInvoiceRequest request, CancellationToken ct) { return _service.Create(request, ct); } }And matching 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:
-
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.
-
-
Create microservice project
- Add
StellaOps.{Domain}.Microserviceusing the skeleton. - Reference domain logic project (
StellaOps.{Domain}.Core), or extract one if necessary.
- Add
-
Map each controller action → endpoint
For each action:
-
Create an endpoint class in the microservice:
-
IRawStellaEndpointfor:- Large payloads.
- Very custom body handling.
-
IStellaEndpoint<TRequest,TResponse>for standard JSON APIs.
-
-
Use
[StellaEndpoint("{METHOD}", "{PATH}")]matching the existing route.
-
-
Wire domain services & auth
- Register the same domain services the WebService used (DB contexts, repositories, etc.).
- Translate role/claim-based
[Authorize]usage to microservice YAMLRequiringClaims.
-
Create microservice YAML
-
For each new endpoint:
- Define default timeout.
supportsStreaming: truewhere appropriate.requiringClaimsmatching prior auth requirements.
-
-
Update router YAML
-
Add service entry under
services:name: "{Domain}".defaultVersion: "1.0.0".
-
Add endpoints (method/path, router-side overrides if needed).
-
-
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.
-
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:
/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 /pingreturns 200 and “pong”.POST /billing/invoicesreturns 200 with a JSON body containing anid.POST /billing/invoices/uploadwith a large payload succeeds and reportsbytesReceived.
-
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.Microserviceexists, runs, and successfully serves requests through the gateway using your real transport (or InMemory/TCP for dev). -
router.billing.yamlplusbilling.microservice.yamlshow config patterns for:- timeouts
- streaming
- requiringClaims
-
docs/router/examples/Billing.Sample.mdexplains how to run and test the example. -
Migration of Webservices to Microservices.mdcontains:- 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.Microserviceor 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.