10 KiB
For this step, the goal is: make StellaOps.Router.Common the single, stable contract layer that everything else can depend on, with no behavior yet, just shapes. After this, gateway, microservice SDK, transports, and config can all compile against it.
Think of this as “lock down the domain vocabulary”.
0. Pre-work
All devs touching Common:
-
Read
docs/router/specs.md, specifically:-
The sections describing:
- Enums (
TransportType,FrameType,InstanceHealthStatus, etc.). - Endpoint/instance/routing models.
- Frames and request/response correlation.
- Routing state and routing plugin.
- Enums (
-
-
Agree that no class/interface will be added to Common if it isn’t in the spec (or discussed with you and then added to the spec).
1. Inventory and file layout
Owner: “Common” lead
-
From
specs.md, extract a type inventory forStellaOps.Router.Common:Enumerations:
TransportTypeFrameTypeInstanceHealthStatus
Core value objects:
ClaimRequirementEndpointDescriptorInstanceDescriptorConnectionStatePayloadLimits(if used from Common; otherwise keep in Config only)- Any small value types you’ve defined (e.g. cancel payload, ping metrics etc. if present in specs).
Routing:
RoutingContextRoutingDecision
Frames:
Frame(type + correlation id + payload)- Optional payload contracts for HELLO, HEARTBEAT, ENDPOINTS_UPDATE, etc., if you’ve specified them explicitly.
Abstractions/interfaces:
IGlobalRoutingStateIRoutingPluginITransportServerITransportClient- Optional:
IRegionProviderif you kept it in the spec.
-
Propose a file layout inside
src/__Libraries/StellaOps.Router.Common:Example:
/StellaOps.Router.Common /Enums TransportType.cs FrameType.cs InstanceHealthStatus.cs /Models ClaimRequirement.cs EndpointDescriptor.cs InstanceDescriptor.cs ConnectionState.cs RoutingContext.cs RoutingDecision.cs Frame.cs /Abstractions IGlobalRoutingState.cs IRoutingPlugin.cs ITransportClient.cs ITransportServer.cs IRegionProvider.cs (if used) -
Get a quick 👍/👎 from you on the layout (no code yet, just file names and namespaces).
2. Implement enums and basic models
Owner: Common dev
Scope: simple, immutable models, no methods.
-
Enums
Implement:
-
TransportTypewith[Udp, Tcp, Certificate, RabbitMq]. -
FrameTypewith:Hello,Heartbeat,EndpointsUpdate,Request,RequestStreamData,Response,ResponseStreamData,Cancel(and any others in specs).
-
InstanceHealthStatuswith:Unknown,Healthy,Degraded,Draining,Unhealthy.
All enums live under
namespace StellaOps.Router.Common;. -
-
Value models
Implement as plain classes/records with auto-properties:
-
ClaimRequirement:string Type(required).string? Value(optional).
-
EndpointDescriptor:string ServiceNamestring Versionstring Methodstring PathTimeSpan DefaultTimeoutbool SupportsStreamingIReadOnlyList<ClaimRequirement> RequiringClaims
-
InstanceDescriptor:string InstanceIdstring ServiceNamestring Versionstring Region
-
ConnectionState:string ConnectionIdInstanceDescriptor InstanceInstanceHealthStatus StatusDateTime LastHeartbeatUtcdouble AveragePingMsTransportType TransportTypeIReadOnlyDictionary<(string Method, string Path), EndpointDescriptor> Endpoints
Design choices:
- Make constructors minimal (empty constructors okay for now).
- Use
initwhere reasonable to encourage immutability for descriptors;ConnectionStatecan have mutable health fields.
-
-
PayloadLimits (if in Common)
If the spec places
PayloadLimitsin Common (versus Config), implement:public sealed class PayloadLimits { public long MaxRequestBytesPerCall { get; set; } public long MaxRequestBytesPerConnection { get; set; } public long MaxAggregateInflightBytes { get; set; } }If it’s defined in Config only, leave it there and avoid duplication.
3. Implement frame & correlation model
Owner: Common dev
-
Implement
Frame:public sealed class Frame { public FrameType Type { get; init; } public Guid CorrelationId { get; init; } public byte[] Payload { get; init; } = Array.Empty<byte>(); } -
If
specs.mddefines specific payload DTOs (e.g.HelloPayload,HeartbeatPayload,CancelPayload), define them too:-
HelloPayload:InstanceDescriptorand list ofEndpointDescriptors, or the equivalent properties.
-
HeartbeatPayload:InstanceId,Status, metrics.
-
CancelPayload:string Reasonor similar.
Keep them as simple DTOs with no logic.
-
-
Do not implement serialization yet (no JSON/MessagePack references here); Common should only define shapes.
4. Routing abstractions
Owner: Common dev
Implement the routing interface + context & decision types.
-
RoutingContext:-
Match the spec. If your
specs.mdversion includesHttpContext, follow it; if you intentionally kept Common free of ASP.NET types, use a neutral context (e.g. method/path/headers/principal). -
For now, if
HttpContextis included in spec, define:public sealed class RoutingContext { public object HttpContext { get; init; } = default!; // or Microsoft.AspNetCore.Http.HttpContext if allowed public EndpointDescriptor Endpoint { get; init; } = default!; public string GatewayRegion { get; init; } = string.Empty; }Then you can refine the type once you finalize whether Common can reference ASP.NET packages. If you want to avoid that now, define your own lightweight context model and let gateway adapt.
-
-
RoutingDecision:-
Must include:
EndpointDescriptor EndpointConnectionState ConnectionTransportType TransportTypeTimeSpan EffectiveTimeout
-
-
IGlobalRoutingState:Interface only, no implementation:
public interface IGlobalRoutingState { EndpointDescriptor? ResolveEndpoint(string method, string path); IReadOnlyList<ConnectionState> GetConnectionsFor( string serviceName, string version, string method, string path); } -
IRoutingPlugin:-
Single method:
public interface IRoutingPlugin { Task<RoutingDecision?> ChooseInstanceAsync( RoutingContext context, CancellationToken cancellationToken); } -
No logic; just interface.
-
5. Transport abstractions
Owner: Common dev
Implement the shared transport contracts.
-
ITransportServer:public interface ITransportServer { Task StartAsync(CancellationToken cancellationToken); Task StopAsync(CancellationToken cancellationToken); } -
ITransportClient:Per spec, you need:
- A buffered call (request → response).
- A streaming call.
- A cancel call.
Interfaces only; content roughly:
public interface ITransportClient { Task<Frame> SendRequestAsync( ConnectionState connection, Frame requestFrame, TimeSpan timeout, CancellationToken cancellationToken); Task SendCancelAsync( ConnectionState connection, Guid correlationId, string? reason = null); Task SendStreamingAsync( ConnectionState connection, Frame requestHeader, Stream requestBody, Func<Stream, Task> readResponseBody, PayloadLimits limits, CancellationToken cancellationToken); }No implementation or transport-specific logic here. No network types beyond
StreamandTask. -
IRegionProvider(if you decided to keep it):public interface IRegionProvider { string Region { get; } }
6. Wire Common into tests (sanity checks only)
Owner: Common tests dev
Create a few very simple unit tests in StellaOps.Router.Common.Tests:
-
Shape tests (these are mostly compile-time):
- That
EndpointDescriptorhas the expected properties and default values can be set. - That
ConnectionStatecan be constructed and that itsEndpointsdictionary handles(Method, Path)keys.
- That
-
Enum completeness tests:
- Assert that
Enum.GetValues(typeof(FrameType))contains all expected values. This catches accidental changes.
- Assert that
-
No behavior yet:
- No routing algorithms or transport behavior tests here; just that model contracts behave like dumb DTOs (e.g. property assignment, default value semantics).
This is mostly to lock in the shape and catch accidental refactors later.
7. Cleanliness & review checklist
Before you move on to the in-memory transport and gateway/microservice wiring, check:
-
StellaOps.Router.Common:- Compiles with zero warnings (nullable enabled).
- Only references BCL; no ASP.NET or serializer packages unless intentionally agreed in the spec.
-
All types listed in
specs.mdunder the Common section exist and match names & property sets. -
No behavior/logic:
- No LINQ-heavy methods.
- No routing algorithm code.
- No network code.
- No YAML/JSON or serialization.
-
StellaOps.Router.Common.Testsruns and passes. -
docs/router/specs.mdis updated if there was any discrepancy (or the code is updated to match the spec, not the other way around).
If you want the next step, I can outline “3. Build in-memory transport + minimal HELLO/REQUEST/RESPONSE wiring” in the same style, so agents can move from contracts to a working vertical slice.