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

15 KiB
Raw Blame History

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:
cd src
dotnet new worker -n StellaOps.Billing.Microservice
  1. 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
  1. 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 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
[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);
    }
}
  1. 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()));
    }
}
  1. 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.
  • RequiringClaims usage.

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:

  1. Load router config and point it to router.billing.yaml for 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;
    });
  1. 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.
  1. 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:
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:

  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:

/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.