router planning

This commit is contained in:
master
2025-12-02 18:38:32 +02:00
parent 790801f329
commit 0c9e8d5d18
15 changed files with 6439 additions and 0 deletions

550
docs/router/11-Step.md Normal file
View File

@@ -0,0 +1,550 @@
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.