551 lines
15 KiB
Markdown
551 lines
15 KiB
Markdown
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<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:
|
||
|
||
* 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<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.
|