17 KiB
For this step you’re wiring configuration into the system properly:
- Router reads a strongly‑typed config model (including payload limits, node region, transports).
- Microservices can optionally load a YAML file to override endpoint metadata discovered by reflection.
- No behavior changes to routing or transports, just how they get their settings.
Think “config plumbing and merging rules,” not new business logic.
0. Preconditions
Before starting, confirm:
-
__Libraries/StellaOps.Router.Configproject exists and referencesStellaOps.Router.Common. -
StellaOps.Microservicehas:StellaMicroserviceOptions(ServiceName, Version, Region, InstanceId, Routers, ConfigFilePath).- Reflection‑based endpoint discovery that produces
EndpointDescriptorinstances.
-
Gateway and microservices currently use hardcoded or stub config; you’re about to replace that with real config.
1. Define RouterConfig model and YAML schema
Project: __Libraries/StellaOps.Router.Config
Owner: config / platform dev
1.1 C# model
Create clear, minimal models to cover current needs (you can extend later):
namespace StellaOps.Router.Config;
public sealed class RouterConfig
{
public GatewayNodeConfig Node { get; set; } = new();
public PayloadLimits PayloadLimits { get; set; } = new();
public IList<TransportEndpointConfig> Transports { get; set; } = new List<TransportEndpointConfig>();
public IList<ServiceConfig> Services { get; set; } = new List<ServiceConfig>();
}
public sealed class GatewayNodeConfig
{
public string NodeId { get; set; } = string.Empty;
public string Region { get; set; } = string.Empty;
public string Environment { get; set; } = "prod";
}
public sealed class TransportEndpointConfig
{
public TransportType TransportType { get; set; }
public int Port { get; set; } // for TCP/UDP/TLS
public bool Enabled { get; set; } = true;
// TLS-specific
public string? ServerCertificatePath { get; set; }
public string? ServerCertificatePassword { get; set; }
public bool RequireClientCertificate { get; set; }
// Rabbit-specific
public string? RabbitConnectionString { get; set; }
}
public sealed class ServiceConfig
{
public string Name { get; set; } = string.Empty;
public string DefaultVersion { get; set; } = "1.0.0";
public IList<string> NeighborRegions { get; set; } = new List<string>();
}
Use the PayloadLimits class from Common (or mirror it here and keep a single definition).
1.2 YAML shape
Decide and document a YAML layout, e.g.:
node:
nodeId: "gw-eu1-01"
region: "eu1"
environment: "prod"
payloadLimits:
maxRequestBytesPerCall: 10485760 # 10 MB
maxRequestBytesPerConnection: 52428800
maxAggregateInflightBytes: 209715200
transports:
- transportType: Tcp
port: 45000
enabled: true
- transportType: Certificate
port: 45001
enabled: false
serverCertificatePath: "certs/router.pfx"
serverCertificatePassword: "secret"
- transportType: Udp
port: 45002
enabled: true
- transportType: RabbitMq
enabled: true
rabbitConnectionString: "amqp://guest:guest@localhost:5672"
services:
- name: "Billing"
defaultVersion: "1.0.0"
neighborRegions: ["eu2", "us1"]
- name: "Identity"
defaultVersion: "2.1.0"
neighborRegions: ["eu2"]
This YAML is the canonical config for the router; environment variables and JSON can override individual properties later via IConfiguration.
2. Implement Router.Config loader and DI extensions
Project: StellaOps.Router.Config
2.1 Choose YAML library
Add a YAML library (e.g. YamlDotNet) to StellaOps.Router.Config:
dotnet add src/__Libraries/StellaOps.Router.Config/StellaOps.Router.Config.csproj package YamlDotNet
2.2 Implement simple loader
Provide a helper that can load YAML into RouterConfig:
public static class RouterConfigLoader
{
public static RouterConfig LoadFromYaml(string path)
{
using var reader = new StreamReader(path);
var yaml = new YamlStream();
yaml.Load(reader);
var root = (YamlMappingNode)yaml.Documents[0].RootNode;
var json = ConvertYamlToJson(root); // simplest: walk node, serialize to JSON string
return JsonSerializer.Deserialize<RouterConfig>(json)!;
}
}
Alternatively, bind YAML directly to RouterConfig with YamlDotNet’s object mapping; the detail is implementation‑specific.
2.3 ASP.NET Core integration extension
In the router library, add a DI extension the gateway can call:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddRouterConfig(
this IServiceCollection services,
IConfiguration configuration)
{
services.Configure<RouterConfig>(configuration.GetSection("Router"));
services.AddSingleton(sp => sp.GetRequiredService<IOptionsMonitor<RouterConfig>>());
return services;
}
}
Gateway will:
- Add the YAML file to the configuration builder.
- Call
AddRouterConfigto bind it.
3. Wire RouterConfig into Gateway startup & components
Project: StellaOps.Gateway.WebService
Owner: gateway dev
3.1 Program.cs configuration
Adjust Program.cs:
var builder = WebApplication.CreateBuilder(args);
// add YAML config
builder.Configuration
.AddJsonFile("appsettings.json", optional: true)
.AddYamlFile("router.yaml", optional: false, reloadOnChange: true)
.AddEnvironmentVariables("STELLAOPS_");
// bind RouterConfig
builder.Services.AddRouterConfig(builder.Configuration.GetSection("Router"));
var app = builder.Build();
Key points:
AddYamlFile("router.yaml", reloadOnChange: true)ensures hot‑reload from YAML.AddEnvironmentVariables("STELLAOPS_")allows env‑based overrides (optional, but useful).
3.2 Inject config into transport factories and routing
Where you start transports:
- Inject
IOptionsMonitor<RouterConfig>into yourITransportServerFactory, and useRouterConfig.Transportsto know which servers to create and on which ports.
Where you need node identity:
-
Inject
IOptionsMonitor<RouterConfig>into any service needingGatewayNodeConfig(e.g. when buildingRoutingContext.GatewayRegion):var nodeRegion = routerConfig.CurrentValue.Node.Region;
Where you need payload limits:
- Inject
IOptionsMonitor<RouterConfig>intoIPayloadBudgetorTransportDispatchMiddlewareto fetch currentPayloadLimits.
Because you’re using IOptionsMonitor, components can react to changes when router.yaml is modified.
4. Microservice YAML: schema & loader
Project: __Libraries/StellaOps.Microservice
Owner: SDK dev
Microservice YAML is optional and used only to override endpoint metadata, not to define identity or router pool.
4.1 Define YAML shape
Keep it focused on endpoints and overrides:
service:
serviceName: "Billing"
version: "1.0.0"
region: "eu1"
endpoints:
- method: "POST"
path: "/billing/invoices/upload"
defaultTimeout: "00:02:00"
supportsStreaming: true
requiringClaims:
- type: "role"
value: "billing-editor"
- method: "GET"
path: "/billing/invoices/{id}"
defaultTimeout: "00:00:10"
requiringClaims:
- type: "role"
value: "billing-reader"
Identity (serviceName, version, region) in YAML is informative; the authoritative values still come from StellaMicroserviceOptions. If they differ, you log, but don’t override options from YAML.
4.2 C# model
In StellaOps.Microservice:
internal sealed class MicroserviceYamlConfig
{
public MicroserviceYamlService? Service { get; set; }
public IList<MicroserviceYamlEndpoint> Endpoints { get; set; } = new List<MicroserviceYamlEndpoint>();
}
internal sealed class MicroserviceYamlService
{
public string? ServiceName { get; set; }
public string? Version { get; set; }
public string? Region { get; set; }
}
internal sealed class MicroserviceYamlEndpoint
{
public string Method { get; set; } = string.Empty;
public string Path { get; set; } = string.Empty;
public string? DefaultTimeout { get; set; }
public bool? SupportsStreaming { get; set; }
public IList<ClaimRequirement> RequiringClaims { get; set; } = new List<ClaimRequirement>();
}
4.3 YAML loader
Reuse YamlDotNet (add package to StellaOps.Microservice if needed):
internal interface IMicroserviceYamlLoader
{
MicroserviceYamlConfig? Load(string? path);
}
internal sealed class MicroserviceYamlLoader : IMicroserviceYamlLoader
{
private readonly ILogger<MicroserviceYamlLoader> _logger;
public MicroserviceYamlLoader(ILogger<MicroserviceYamlLoader> logger)
{
_logger = logger;
}
public MicroserviceYamlConfig? Load(string? path)
{
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
return null;
try
{
using var reader = new StreamReader(path);
var deserializer = new DeserializerBuilder().Build();
return deserializer.Deserialize<MicroserviceYamlConfig>(reader);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load microservice YAML from {Path}", path);
return null;
}
}
}
Register in DI:
services.AddSingleton<IMicroserviceYamlLoader, MicroserviceYamlLoader>();
5. Merge YAML overrides with reflection-discovered endpoints
Project: StellaOps.Microservice
Owner: SDK dev
Extend EndpointCatalog to apply YAML overrides.
5.1 Extend constructor to accept YAML config
Adjust EndpointCatalog:
internal sealed class EndpointCatalog : IEndpointCatalog
{
public IReadOnlyList<EndpointDescriptor> Descriptors { get; }
private readonly Dictionary<(string Method, string Path), EndpointRegistration> _map;
public EndpointCatalog(
IEndpointDiscovery discovery,
IMicroserviceYamlLoader yamlLoader,
IOptions<StellaMicroserviceOptions> optionsAccessor)
{
var options = optionsAccessor.Value;
var registrations = discovery.DiscoverEndpoints(options);
var yamlConfig = yamlLoader.Load(options.ConfigFilePath);
registrations = ApplyYamlOverrides(registrations, yamlConfig);
_map = registrations.ToDictionary(
r => (r.Descriptor.Method, r.Descriptor.Path),
r => r,
StringComparer.OrdinalIgnoreCase);
Descriptors = registrations.Select(r => r.Descriptor).ToArray();
}
}
5.2 Implement ApplyYamlOverrides
Key rules:
-
Identity (ServiceName, Version, Region) always come from
StellaMicroserviceOptions. -
YAML can override:
DefaultTimeoutSupportsStreamingRequiringClaims
Implementation sketch:
private static IReadOnlyList<EndpointRegistration> ApplyYamlOverrides(
IReadOnlyList<EndpointRegistration> registrations,
MicroserviceYamlConfig? yaml)
{
if (yaml is null || yaml.Endpoints.Count == 0)
return registrations;
var overrideMap = yaml.Endpoints.ToDictionary(
e => (e.Method, e.Path),
e => e,
StringComparer.OrdinalIgnoreCase);
var result = new List<EndpointRegistration>(registrations.Count);
foreach (var reg in registrations)
{
if (!overrideMap.TryGetValue((reg.Descriptor.Method, reg.Descriptor.Path), out var ov))
{
result.Add(reg);
continue;
}
var desc = reg.Descriptor;
var timeout = desc.DefaultTimeout;
if (!string.IsNullOrWhiteSpace(ov.DefaultTimeout) &&
TimeSpan.TryParse(ov.DefaultTimeout, out var parsed))
{
timeout = parsed;
}
var supportsStreaming = desc.SupportsStreaming;
if (ov.SupportsStreaming.HasValue)
{
supportsStreaming = ov.SupportsStreaming.Value;
}
var requiringClaims = ov.RequiringClaims.Count > 0
? ov.RequiringClaims.ToArray()
: desc.RequiringClaims;
var overriddenDescriptor = new EndpointDescriptor
{
ServiceName = desc.ServiceName,
Version = desc.Version,
Method = desc.Method,
Path = desc.Path,
DefaultTimeout = timeout,
SupportsStreaming = supportsStreaming,
RequiringClaims = requiringClaims
};
result.Add(new EndpointRegistration
{
Descriptor = overriddenDescriptor,
HandlerType = reg.HandlerType
});
}
return result;
}
This ensures code defines the set of endpoints; YAML only tunes metadata.
6. Hot‑reload / YAML change handling
Router side: you already enabled reloadOnChange for router.yaml, and use IOptionsMonitor<RouterConfig>. Next:
-
Components that care about changes must react:
-
Payload limits:
IPayloadBudgetorTransportDispatchMiddlewareshould readrouterConfig.CurrentValue.PayloadLimitson each request rather than caching.
-
Node region:
RoutingContext.GatewayRegioncan be built fromrouterConfig.CurrentValue.Node.Regionper request.
-
You do not need a custom watcher; IOptionsMonitor already tracks config changes.
Microservice side: for now you can start with load-on-startup YAML. If you want hot‑reload:
-
Implement a FileSystemWatcher in
MicroserviceYamlLoaderor a smallIHostedService:-
Watch
options.ConfigFilePathfor changes. -
On change:
- Reload YAML.
- Rebuild
EndpointDescriptorlist. - Send an updated HELLO or an ENDPOINTS_UPDATE frame to router.
-
Given complexity, you can postpone true hot reload to a later iteration and document that microservices must be restarted to pick up YAML changes.
7. Tests
Router.Config tests:
-
Unit tests for
RouterConfigLoader:- Given a YAML string, bind to
RouterConfigproperly. - Validate
TransportType.Tcp/Udp/RabbitMqvalues map correctly.
- Given a YAML string, bind to
-
Integration test:
- Start gateway with
router.yaml. - Access
IOptionsMonitor<RouterConfig>in a test controller or test service and assert values. - Modify YAML on disk (if test infra allows) and ensure values update via
IOptionsMonitor.
- Start gateway with
Microservice YAML tests:
-
Unit tests for
MicroserviceYamlLoader:- Load valid YAML, confirm endpoints and claims/timeouts parsed.
-
EndpointCatalogtests:-
Build fake
EndpointRegistrationlist from reflection. -
Build YAML overrides.
-
Call
ApplyYamlOverridesand assert:- Timeouts updated.
- SupportsStreaming updated.
- RequiringClaims replaced where provided.
- Descriptors with no matching YAML remain unchanged.
-
8. Documentation updates
Update docs under docs/router:
-
Stella Ops Router – Webserver.md:
-
Describe
router.yaml:- Node config (region, nodeId).
- PayloadLimits.
- Transports.
-
Explain precedence:
- YAML as base.
- Environment variables can override individual fields via
STELLAOPS_Router__Node__Regionetc.
-
-
Stella Ops Router – Microservice.md:
-
Explain
ConfigFilePathinStellaMicroserviceOptions. -
Show full example microservice YAML and how it maps to endpoint metadata.
-
Clearly state:
- Identity comes from options (code/config), not YAML.
- YAML can override per‑endpoint timeout, streaming flag, requiringClaims.
- YAML can’t add endpoints that don’t exist in code.
-
-
Stella Ops Router Documentation.md:
-
Add a short “Configuration” chapter:
- Where
router.yamllives. - Where microservice YAML lives.
- How to run locally with custom configs.
- Where
-
9. Done criteria for “Add Router.Config + Microservice YAML integration”
You can call step 10 complete when:
-
Router:
- Loads
router.yamlintoRouterConfigusingStellaOps.Router.Config. - Uses
RouterConfig.Node.Regionwhen building routing context. - Uses
RouterConfig.PayloadLimitsfor payload budget enforcement. - Uses
RouterConfig.Transportsto start the rightITransportServerinstances. - Supports runtime changes to
router.yamlviaIOptionsMonitorfor at least node identity and payload limits.
- Loads
-
Microservice:
- Accepts optional
ConfigFilePathinStellaMicroserviceOptions. - Loads YAML (when present) and merges overrides into reflection‑discovered endpoints.
- Sends HELLO with the merged descriptors (i.e., YAML-aware defaults).
- Behavior remains unchanged when no YAML is provided (pure reflection mode).
- Accepts optional
-
Tests:
- Confirm config binding for router and microservice.
- Confirm YAML overrides are applied correctly to endpoint metadata.
At that point, configuration is no longer hardcoded, and you have a clear, documented path for both router operators and microservice teams to configure behavior via YAML with predictable precedence.