Files
git.stella-ops.org/docs/router/11-Step.md
2025-12-02 18:38:32 +02:00

551 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.
Ill 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 gateways 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 34 endpoints that exercise different features:
1. **Health / contract check**
```csharp
[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);
}
}
```
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<CreateInvoiceRequest, CreateInvoiceResponse>
{
public Task<CreateInvoiceResponse> 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<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`
```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<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;
});
```
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:
* WebServices 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<ActionResult<InvoiceDto>> Create(CreateInvoiceRequest request)
{
var result = await _service.Create(request);
return Ok(result);
}
}
```
Microservice endpoint:
```csharp
[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:
```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<TRequest,TResponse>` 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.