For this step, the goal is: a microservice that can: * Start up with `AddStellaMicroservice(...)` * Discover its endpoints from attributes * Connect to the router (via InMemory transport) * Send a HELLO with identity + endpoints * Receive a REQUEST and return a RESPONSE No streaming, no cancellation, no heartbeat yet. Pure minimal handshake & dispatch. --- ## 0. Preconditions Before your agents start this step, you should have: * `StellaOps.Router.Common` contracts in place (enums, `EndpointDescriptor`, `ConnectionState`, `Frame`, etc.). * The solution skeleton and project references configured. * A **stub** InMemory transport “router harness” (at least a place to park the future InMemory transport). Even if it’s not fully implemented, assume it will expose: * A way for a microservice to “connect” and register itself. * A way to deliver frames from router to microservice and back. If InMemory isn’t built yet, the microservice code should be written *against abstractions* so you can plug it in later. --- ## 1. Define microservice public surface (SDK contract) **Project:** `__Libraries/StellaOps.Microservice` **Owner:** microservice SDK agent Purpose: give product teams a stable way to define services and endpoints without caring about transports. ### 1.1 Options Make sure `StellaMicroserviceOptions` matches the spec: ```csharp public sealed class StellaMicroserviceOptions { public string ServiceName { get; set; } = string.Empty; public string Version { get; set; } = string.Empty; public string Region { get; set; } = string.Empty; public string InstanceId { get; set; } = string.Empty; public IList Routers { get; set; } = new List(); public string? ConfigFilePath { get; set; } } public sealed class RouterEndpointConfig { public string Host { get; set; } = string.Empty; public int Port { get; set; } public TransportType TransportType { get; set; } } ``` `Routers` is mandatory: without at least one router configured, the SDK should refuse to start later (that policy can be enforced in the handshake stage). ### 1.2 Public endpoint abstractions Define: * Attribute for endpoint identity: ```csharp [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public sealed class StellaEndpointAttribute : Attribute { public string Method { get; } public string Path { get; } public StellaEndpointAttribute(string method, string path) { Method = method; Path = path; } } ``` * Raw handler: ```csharp public sealed class RawRequestContext { public string Method { get; init; } = string.Empty; public string Path { get; init; } = string.Empty; public IReadOnlyDictionary Headers { get; init; } = new Dictionary(); public Stream Body { get; init; } = Stream.Null; public CancellationToken CancellationToken { get; init; } } public sealed class RawResponse { public int StatusCode { get; set; } = 200; public IDictionary Headers { get; } = new Dictionary(); public Func? WriteBodyAsync { get; set; } // may be null } public interface IRawStellaEndpoint { Task HandleAsync(RawRequestContext ctx); } ``` * Typed convenience interfaces (used later, but define now): ```csharp public interface IStellaEndpoint { Task HandleAsync(TRequest request, CancellationToken ct); } public interface IStellaEndpoint { Task HandleAsync(CancellationToken ct); } ``` At this step, you don’t need to implement adapters yet, but the signatures must be fixed. ### 1.3 Registration extension Extend `AddStellaMicroservice` to wire options + a few internal services: ```csharp public static class ServiceCollectionExtensions { public static IServiceCollection AddStellaMicroservice( this IServiceCollection services, Action configure) { services.Configure(configure); services.AddSingleton(); // to be implemented services.AddSingleton(); // to be implemented services.AddHostedService(); // handshake loop return services; } } ``` This still compiles with empty implementations; you fill them in next steps. --- ## 2. Endpoint discovery (reflection only for now) **Project:** `StellaOps.Microservice` **Owner:** SDK agent Goal: given the entry assembly, build: * A list of `EndpointDescriptor` objects (from Common). * A mapping `(Method, Path) -> handler type` used for dispatch. ### 2.1 Internal types Define an internal representation: ```csharp internal sealed class EndpointRegistration { public EndpointDescriptor Descriptor { get; init; } = default!; public Type HandlerType { get; init; } = default!; } ``` Define an interface for discovery: ```csharp internal interface IEndpointDiscovery { IReadOnlyList DiscoverEndpoints(StellaMicroserviceOptions options); } ``` ### 2.2 Implement reflection-based discovery Create `ReflectionEndpointDiscovery`: * Scan the entry assembly (and optionally referenced assemblies) for classes that: * Have `StellaEndpointAttribute`. * Implement either: * `IRawStellaEndpoint`, or * `IStellaEndpoint<,>`, or * `IStellaEndpoint<>`. * For each `[StellaEndpoint]` usage: * Create `EndpointDescriptor` with: * `ServiceName` = `options.ServiceName`. * `Version` = `options.Version`. * `Method`, `Path` from attribute. * `DefaultTimeout` = some sensible default (e.g. `TimeSpan.FromSeconds(30)`; refine later). * `SupportsStreaming` = `false` (for now). * `RequiringClaims` = empty array (for now). * Create `EndpointRegistration` with `Descriptor` + `HandlerType`. * Return the list. Wire it into DI: ```csharp services.AddSingleton(); ``` --- ## 3. Endpoint catalog & dispatcher (microservice internal) **Project:** `StellaOps.Microservice` **Owner:** SDK agent Goal: presence of: * A catalog holding endpoints and descriptors. * A dispatcher that takes frames and calls handlers. ### 3.1 Endpoint catalog Define: ```csharp internal interface IEndpointCatalog { IReadOnlyList Descriptors { get; } bool TryGetHandler(string method, string path, out EndpointRegistration endpoint); } internal sealed class EndpointCatalog : IEndpointCatalog { private readonly Dictionary<(string Method, string Path), EndpointRegistration> _map; public IReadOnlyList Descriptors { get; } public EndpointCatalog(IEndpointDiscovery discovery, IOptions optionsAccessor) { var options = optionsAccessor.Value; var registrations = discovery.DiscoverEndpoints(options); _map = registrations.ToDictionary( r => (r.Descriptor.Method, r.Descriptor.Path), r => r, StringComparer.OrdinalIgnoreCase); Descriptors = registrations.Select(r => r.Descriptor).ToArray(); } public bool TryGetHandler(string method, string path, out EndpointRegistration endpoint) => _map.TryGetValue((method, path), out endpoint!); } ``` You can refine path normalization later; for now, keep it simple. ### 3.2 Endpoint dispatcher Define: ```csharp internal interface IEndpointDispatcher { Task HandleRequestAsync(Frame requestFrame, CancellationToken ct); } ``` Implement `EndpointDispatcher` with minimal behavior: 1. Decode `requestFrame.Payload` into a small DTO carrying: * Method * Path * Headers (if you already have a format; if not, assume no headers in v0) * Body bytes For this step, you can stub decoding as: * Payload = raw body bytes. * Method/Path are carried separately in frame header or in a simple DTO; decide a minimal interim format and write it down. 2. Use `IEndpointCatalog.TryGetHandler(method, path, ...)`: * If not found: * Build a `RawResponse` with status 404 and empty body. 3. If handler implements `IRawStellaEndpoint`: * Instantiate via DI (`IServiceProvider.GetRequiredService(handlerType)`). * Build `RawRequestContext` with: * Method, Path, Headers, Body (`new MemoryStream(bodyBytes)` for now). * `CancellationToken` = `ct`. * Call `HandleAsync`. * Convert `RawResponse` into a response frame payload. 4. If handler implements `IStellaEndpoint<,>` (typed): * For now, **you can skip typed handling** or wire a very simple JSON-based adapter if you want to unlock it early. The focus in this step is the raw path; typed adapters can come in the next iteration. Return a `Frame` with: * `Type = FrameType.Response` * `CorrelationId` = `requestFrame.CorrelationId` * `Payload` = encoded response (status + body bytes). No streaming, no cancellation logic beyond passing `ct` through — router won’t cancel yet. --- ## 4. Minimal handshake hosted service (using InMemory) **Project:** `StellaOps.Microservice` **Owner:** SDK agent This is where the microservice actually “talks” to the router. ### 4.1 Define a microservice connection abstraction Your SDK should not depend directly on InMemory; define an internal abstraction: ```csharp internal interface IMicroserviceConnection { Task StartAsync(CancellationToken ct); Task StopAsync(CancellationToken ct); } ``` The implementation for this step will target the InMemory transport; later you can add TCP/TLS/RabbitMQ versions. ### 4.2 Implement InMemory microservice connection Assuming you have or will have an `IInMemoryRouter` (or similar) dev harness, implement: ```csharp internal sealed class InMemoryMicroserviceConnection : IMicroserviceConnection { private readonly IEndpointCatalog _catalog; private readonly IEndpointDispatcher _dispatcher; private readonly IOptions _options; private readonly IInMemoryRouterClient _routerClient; // dev-only abstraction public InMemoryMicroserviceConnection( IEndpointCatalog catalog, IEndpointDispatcher dispatcher, IOptions options, IInMemoryRouterClient routerClient) { _catalog = catalog; _dispatcher = dispatcher; _options = options; _routerClient = routerClient; } public async Task StartAsync(CancellationToken ct) { var opts = _options.Value; // Build HELLO payload from options + catalog.Descriptors var helloPayload = BuildHelloPayload(opts, _catalog.Descriptors); await _routerClient.ConnectAsync(opts, ct); await _routerClient.SendHelloAsync(helloPayload, ct); // Start background receive loop _ = Task.Run(() => ReceiveLoopAsync(ct), ct); } public Task StopAsync(CancellationToken ct) { // For now: ask routerClient to disconnect; finer handling later return _routerClient.DisconnectAsync(ct); } private async Task ReceiveLoopAsync(CancellationToken ct) { await foreach (var frame in _routerClient.GetIncomingFramesAsync(ct)) { if (frame.Type == FrameType.Request) { var response = await _dispatcher.HandleRequestAsync(frame, ct); await _routerClient.SendFrameAsync(response, ct); } else { // Ignore other frame types in this minimal step } } } } ``` `IInMemoryRouterClient` is whatever dev harness you build for the in-memory transport; the exact shape is not important for this step’s planning, only that it provides: * `ConnectAsync` * `SendHelloAsync` * `GetIncomingFramesAsync` (async stream of frames) * `SendFrameAsync` for responses * `DisconnectAsync` ### 4.3 Hosted service to bootstrap the connection Implement `MicroserviceBootstrapHostedService`: ```csharp internal sealed class MicroserviceBootstrapHostedService : IHostedService { private readonly IMicroserviceConnection _connection; public MicroserviceBootstrapHostedService(IMicroserviceConnection connection) { _connection = connection; } public Task StartAsync(CancellationToken cancellationToken) => _connection.StartAsync(cancellationToken); public Task StopAsync(CancellationToken cancellationToken) => _connection.StopAsync(cancellationToken); } ``` Wire `IMicroserviceConnection` to `InMemoryMicroserviceConnection` in DI for now: ```csharp services.AddSingleton(); ``` In a later phase, you’ll swap this to transport-specific connectors. --- ## 5. End-to-end smoke test (InMemory only) **Project:** `StellaOps.Microservice.Tests` + a minimal InMemory router test harness **Owner:** test agent Goal: prove that minimal handshake & dispatch works in memory. 1. Build a trivial test microservice: * Define a handler: ```csharp [StellaEndpoint("GET", "/ping")] public sealed class PingEndpoint : IRawStellaEndpoint { public Task HandleAsync(RawRequestContext ctx) { var resp = new RawResponse { StatusCode = 200 }; resp.Headers["Content-Type"] = "text/plain"; resp.WriteBodyAsync = stream => stream.WriteAsync( Encoding.UTF8.GetBytes("pong")); return Task.FromResult(resp); } } ``` 2. Test harness: * Spin up: * An instance of the microservice host (generic HostBuilder). * An in-memory “router” that: * Accepts HELLO from the microservice. * Sends a single REQUEST frame for `GET /ping`. * Receives the RESPONSE frame. 3. Assert: * The HELLO includes the `/ping` endpoint. * The REQUEST is dispatched to `PingEndpoint`. * The RESPONSE has status 200 and body “pong”. This verifies that: * `AddStellaMicroservice` wires discovery, catalog, dispatcher, bootstrap. * The microservice sends HELLO on connect. * The microservice can handle at least one request via InMemory. --- ## 6. Done criteria for “minimal handshake & dispatch” You can consider this step complete when: * `StellaOps.Microservice` exposes: * Options. * Attribute & handler interfaces (raw + typed). * `AddStellaMicroservice` registering discovery, catalog, dispatcher, and hosted service. * The microservice can: * Discover endpoints via reflection. * Build a `HELLO` payload and send it over InMemory on startup. * Receive a `REQUEST` frame over InMemory. * Dispatch that request to the correct handler. * Return a `RESPONSE` frame. Not yet required in this step: * Streaming bodies. * Heartbeats or health evaluation. * Cancellation via CANCEL frames. * Authority overrides for requiringClaims. Those come in subsequent phases; right now you just want a working minimal vertical slice: InMemory microservice that says “HELLO” and responds to one simple request.