# Step 19: Microservice Host Builder **Phase 5: Microservice SDK** **Estimated Complexity:** High **Dependencies:** Step 14 (TCP Transport), Step 15 (TLS Transport) --- ## Overview The Microservice Host Builder provides a fluent API for building microservices that connect to the Stella Router. It handles transport configuration, endpoint registration, graceful shutdown, and integration with ASP.NET Core's hosting infrastructure. --- ## Goals 1. Provide fluent builder API for microservice configuration 2. Support both standalone and ASP.NET Core integrated hosting 3. Handle transport lifecycle (connect, reconnect, disconnect) 4. Support multiple transport configurations 5. Enable dual-exposure mode (gateway + direct HTTP) --- ## Core Architecture ``` ┌────────────────────────────────────────────────────────────────┐ │ Microservice Host Builder │ ├────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ StellaMicroserviceHost │ │ │ │ ┌───────────────┐ ┌───────────────┐ ┌─────────────┐ │ │ │ │ │Transport Layer│ │Endpoint Registry│ │ Request │ │ │ │ │ │ (TCP/TLS/etc) │ │(Discovery/Reg) │ │ Dispatcher │ │ │ │ │ └───────────────┘ └───────────────┘ └─────────────┘ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Optional: ASP.NET Core Host │ │ │ │ (Kestrel for direct HTTP access + default claims) │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────────────┘ ``` --- ## Configuration ```csharp namespace StellaOps.Microservice; public class StellaMicroserviceOptions { /// Service name for registration. public required string ServiceName { get; set; } /// Unique instance identifier (auto-generated if not set). public string InstanceId { get; set; } = Guid.NewGuid().ToString("N")[..8]; /// Service version for routing. public string Version { get; set; } = "1.0.0"; /// Region for routing affinity. public string? Region { get; set; } /// Tags for routing metadata. public Dictionary Tags { get; set; } = new(); /// Router connection pool. public List Routers { get; set; } = new(); /// Transport configuration. public TransportConfig Transport { get; set; } = new(); /// Endpoint discovery configuration. public EndpointDiscoveryConfig Discovery { get; set; } = new(); /// Heartbeat configuration. public HeartbeatConfig Heartbeat { get; set; } = new(); /// Dual exposure mode configuration. public DualExposureConfig? DualExposure { get; set; } /// Graceful shutdown timeout. public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(30); } public class RouterConnectionConfig { public string Host { get; set; } = "localhost"; public int Port { get; set; } = 9500; public string Transport { get; set; } = "TCP"; // TCP, TLS, InMemory public int Priority { get; set; } = 1; public bool Enabled { get; set; } = true; } public class TransportConfig { public string Default { get; set; } = "TCP"; public TcpClientConfig? Tcp { get; set; } public TlsClientConfig? Tls { get; set; } public int MaxReconnectAttempts { get; set; } = -1; // -1 = unlimited public TimeSpan ReconnectDelay { get; set; } = TimeSpan.FromSeconds(5); } public class EndpointDiscoveryConfig { /// Assemblies to scan for endpoints. public List ScanAssemblies { get; set; } = new(); /// Path to YAML overrides file. public string? ConfigFilePath { get; set; } /// Base path prefix for all endpoints. public string? BasePath { get; set; } /// Whether to auto-discover endpoints via reflection. public bool AutoDiscover { get; set; } = true; } public class HeartbeatConfig { public TimeSpan Interval { get; set; } = TimeSpan.FromSeconds(10); public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(5); public int MissedHeartbeatsThreshold { get; set; } = 3; } public class DualExposureConfig { /// Enable direct HTTP access. public bool Enabled { get; set; } = false; /// HTTP port for direct access. public int HttpPort { get; set; } = 8080; /// Default claims for direct access (no JWT). public Dictionary DefaultClaims { get; set; } = new(); /// Whether to require JWT for direct access. public bool RequireAuthentication { get; set; } = false; } ``` --- ## Host Builder Implementation ```csharp namespace StellaOps.Microservice; public interface IStellaMicroserviceBuilder { IStellaMicroserviceBuilder ConfigureServices(Action configure); IStellaMicroserviceBuilder ConfigureTransport(Action configure); IStellaMicroserviceBuilder ConfigureEndpoints(Action configure); IStellaMicroserviceBuilder AddRouter(string host, int port, string transport = "TCP"); IStellaMicroserviceBuilder EnableDualExposure(Action? configure = null); IStellaMicroserviceBuilder UseYamlConfig(string path); IStellaMicroserviceHost Build(); } public sealed class StellaMicroserviceBuilder : IStellaMicroserviceBuilder { private readonly StellaMicroserviceOptions _options; private readonly IServiceCollection _services; private readonly List> _configureActions = new(); public StellaMicroserviceBuilder(string serviceName) { _options = new StellaMicroserviceOptions { ServiceName = serviceName }; _services = new ServiceCollection(); // Add default services _services.AddLogging(b => b.AddConsole()); _services.AddSingleton(_options); } public static IStellaMicroserviceBuilder Create(string serviceName) { return new StellaMicroserviceBuilder(serviceName); } public IStellaMicroserviceBuilder ConfigureServices(Action configure) { _configureActions.Add(configure); return this; } public IStellaMicroserviceBuilder ConfigureTransport(Action configure) { configure(_options.Transport); return this; } public IStellaMicroserviceBuilder ConfigureEndpoints(Action configure) { configure(_options.Discovery); return this; } public IStellaMicroserviceBuilder AddRouter(string host, int port, string transport = "TCP") { _options.Routers.Add(new RouterConnectionConfig { Host = host, Port = port, Transport = transport, Priority = _options.Routers.Count + 1 }); return this; } public IStellaMicroserviceBuilder EnableDualExposure(Action? configure = null) { _options.DualExposure = new DualExposureConfig { Enabled = true }; configure?.Invoke(_options.DualExposure); return this; } public IStellaMicroserviceBuilder UseYamlConfig(string path) { _options.Discovery.ConfigFilePath = path; return this; } public IStellaMicroserviceHost Build() { // Apply custom service configuration foreach (var action in _configureActions) { action(_services); } // Add core services AddCoreServices(); // Add transport services AddTransportServices(); // Add endpoint services AddEndpointServices(); var serviceProvider = _services.BuildServiceProvider(); return serviceProvider.GetRequiredService(); } private void AddCoreServices() { _services.AddSingleton(); _services.AddSingleton(); _services.AddSingleton(); _services.AddSingleton(); } private void AddTransportServices() { _services.AddSingleton(); switch (_options.Transport.Default.ToUpper()) { case "TCP": _services.AddSingleton(); break; case "TLS": _services.AddSingleton(); _services.AddSingleton(); break; case "INMEMORY": // InMemory requires hub to be provided externally _services.AddSingleton(); break; } } private void AddEndpointServices() { _services.AddSingleton(); if (!string.IsNullOrEmpty(_options.Discovery.ConfigFilePath)) { _services.AddSingleton(); } } } ``` --- ## Microservice Host Implementation ```csharp namespace StellaOps.Microservice; public interface IStellaMicroserviceHost : IAsyncDisposable { StellaMicroserviceOptions Options { get; } bool IsConnected { get; } Task StartAsync(CancellationToken cancellationToken = default); Task StopAsync(CancellationToken cancellationToken = default); Task WaitForShutdownAsync(CancellationToken cancellationToken = default); } public sealed class StellaMicroserviceHost : IStellaMicroserviceHost, IHostedService { private readonly StellaMicroserviceOptions _options; private readonly ITransportServer _transport; private readonly IEndpointRegistry _endpointRegistry; private readonly IRequestDispatcher _dispatcher; private readonly ILogger _logger; private readonly CancellationTokenSource _shutdownCts = new(); private readonly TaskCompletionSource _shutdownComplete = new(); private Timer? _heartbeatTimer; private IHost? _httpHost; public StellaMicroserviceOptions Options => _options; public bool IsConnected => _transport.IsConnected; public StellaMicroserviceHost( StellaMicroserviceOptions options, ITransportServer transport, IEndpointRegistry endpointRegistry, IRequestDispatcher dispatcher, ILogger logger) { _options = options; _transport = transport; _endpointRegistry = endpointRegistry; _dispatcher = dispatcher; _logger = logger; } public async Task StartAsync(CancellationToken cancellationToken = default) { _logger.LogInformation( "Starting microservice {ServiceName}/{InstanceId}", _options.ServiceName, _options.InstanceId); // Discover endpoints var endpoints = await _endpointRegistry.DiscoverEndpointsAsync(cancellationToken); _logger.LogInformation("Discovered {Count} endpoints", endpoints.Length); // Wire up request handler _transport.OnRequest += HandleRequestAsync; _transport.OnCancel += HandleCancelAsync; // Connect to router var router = _options.Routers.OrderBy(r => r.Priority).FirstOrDefault() ?? throw new InvalidOperationException("No routers configured"); await _transport.ConnectAsync( _options.ServiceName, _options.InstanceId, endpoints, cancellationToken); _logger.LogInformation( "Connected to router at {Host}:{Port}", router.Host, router.Port); // Start heartbeat _heartbeatTimer = new Timer( SendHeartbeatAsync, null, _options.Heartbeat.Interval, _options.Heartbeat.Interval); // Start dual exposure HTTP if enabled if (_options.DualExposure?.Enabled == true) { await StartHttpHostAsync(cancellationToken); } _logger.LogInformation( "Microservice {ServiceName} started successfully", _options.ServiceName); } private async Task HandleRequestAsync( RequestPayload request, CancellationToken cancellationToken) { using var activity = Activity.StartActivity("HandleRequest"); activity?.SetTag("http.method", request.Method); activity?.SetTag("http.path", request.Path); try { return await _dispatcher.DispatchAsync(request, cancellationToken); } catch (Exception ex) { _logger.LogError(ex, "Error handling request {Path}", request.Path); return new ResponsePayload { StatusCode = 500, Headers = new Dictionary(), Body = Encoding.UTF8.GetBytes($"{{\"error\": \"{ex.Message}\"}}"), IsFinalChunk = true }; } } private Task HandleCancelAsync(string correlationId, CancellationToken cancellationToken) { _logger.LogDebug("Request {CorrelationId} cancelled", correlationId); // Propagate cancellation to active request handling return Task.CompletedTask; } private async void SendHeartbeatAsync(object? state) { try { await _transport.SendHeartbeatAsync(_shutdownCts.Token); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to send heartbeat"); } } private async Task StartHttpHostAsync(CancellationToken cancellationToken) { var config = _options.DualExposure!; _httpHost = Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(web => { web.UseKestrel(k => k.ListenAnyIP(config.HttpPort)); web.Configure(app => { app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapFallback(async context => { // Inject default claims for direct access var claims = config.DefaultClaims; var request = new RequestPayload { Method = context.Request.Method, Path = context.Request.Path + context.Request.QueryString, Host = context.Request.Host.Value, Headers = context.Request.Headers .ToDictionary(h => h.Key, h => h.Value.ToString()), Claims = claims, ClientIp = context.Connection.RemoteIpAddress?.ToString(), TraceId = context.TraceIdentifier }; // Read body if present if (context.Request.ContentLength > 0) { using var ms = new MemoryStream(); await context.Request.Body.CopyToAsync(ms); request = request with { Body = ms.ToArray() }; } var response = await _dispatcher.DispatchAsync(request, context.RequestAborted); context.Response.StatusCode = response.StatusCode; foreach (var (key, value) in response.Headers) { context.Response.Headers[key] = value; } if (response.Body != null) { await context.Response.Body.WriteAsync(response.Body); } }); }); }); }) .Build(); await _httpHost.StartAsync(cancellationToken); _logger.LogInformation( "Direct HTTP access enabled on port {Port}", config.HttpPort); } public async Task StopAsync(CancellationToken cancellationToken = default) { _logger.LogInformation( "Stopping microservice {ServiceName}", _options.ServiceName); _shutdownCts.Cancel(); _heartbeatTimer?.Dispose(); if (_httpHost != null) { await _httpHost.StopAsync(cancellationToken); } await _transport.DisconnectAsync(); _logger.LogInformation( "Microservice {ServiceName} stopped", _options.ServiceName); _shutdownComplete.TrySetResult(); } public Task WaitForShutdownAsync(CancellationToken cancellationToken = default) { return _shutdownComplete.Task.WaitAsync(cancellationToken); } public async ValueTask DisposeAsync() { await StopAsync(); _shutdownCts.Dispose(); } // IHostedService implementation for ASP.NET Core integration Task IHostedService.StartAsync(CancellationToken cancellationToken) => StartAsync(cancellationToken); Task IHostedService.StopAsync(CancellationToken cancellationToken) => StopAsync(cancellationToken); } ``` --- ## ASP.NET Core Integration ```csharp namespace StellaOps.Microservice; public static class StellaMicroserviceExtensions { /// /// Adds Stella microservice to an existing ASP.NET Core host. /// public static IServiceCollection AddStellaMicroservice( this IServiceCollection services, Action configure) { var options = new StellaMicroserviceOptions { ServiceName = "unknown" }; configure(options); services.AddSingleton(options); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // Add transport based on configuration switch (options.Transport.Default.ToUpper()) { case "TCP": services.AddSingleton(); break; case "TLS": services.AddSingleton(); services.AddSingleton(); break; } services.AddSingleton(); services.AddHostedService(sp => (StellaMicroserviceHost)sp.GetRequiredService()); return services; } /// /// Configures an endpoint handler for the microservice. /// public static IServiceCollection AddEndpointHandler( this IServiceCollection services) where THandler : class, IEndpointHandler { services.AddScoped(); return services; } } ``` --- ## Usage Examples ### Standalone Microservice ```csharp var host = StellaMicroserviceBuilder .Create("billing-service") .AddRouter("gateway.internal", 9500, "TLS") .ConfigureTransport(t => { t.Tls = new TlsClientConfig { ClientCertificatePath = "/etc/certs/billing.pfx", ClientCertificatePassword = Environment.GetEnvironmentVariable("CERT_PASSWORD") }; }) .ConfigureEndpoints(e => { e.BasePath = "/billing"; e.ScanAssemblies.Add("BillingService.Handlers"); }) .ConfigureServices(services => { services.AddScoped(); services.AddScoped(); }) .Build(); await host.StartAsync(); await host.WaitForShutdownAsync(); ``` ### ASP.NET Core Integration ```csharp var builder = WebApplication.CreateBuilder(args); builder.Services.AddStellaMicroservice(options => { options.ServiceName = "user-service"; options.Region = "us-east-1"; options.Routers.Add(new RouterConnectionConfig { Host = "gateway.internal", Port = 9500 }); options.DualExposure = new DualExposureConfig { Enabled = true, HttpPort = 8080, DefaultClaims = new Dictionary { ["tier"] = "free" } }; }); builder.Services.AddEndpointHandler(); var app = builder.Build(); await app.RunAsync(); ``` --- ## YAML Configuration ```yaml Microservice: ServiceName: "billing-service" Version: "1.0.0" Region: "us-east-1" Tags: team: "payments" tier: "critical" Routers: - Host: "gateway-primary.internal" Port: 9500 Transport: "TLS" Priority: 1 - Host: "gateway-secondary.internal" Port: 9500 Transport: "TLS" Priority: 2 Transport: Default: "TLS" Tls: ClientCertificatePath: "/etc/certs/service.pfx" ClientCertificatePassword: "${CERT_PASSWORD}" Discovery: AutoDiscover: true BasePath: "/billing" ConfigFilePath: "/etc/stellaops/endpoints.yaml" Heartbeat: Interval: "00:00:10" Timeout: "00:00:05" DualExposure: Enabled: true HttpPort: 8080 DefaultClaims: tier: "free" ShutdownTimeout: "00:00:30" ``` --- ## Deliverables 1. `StellaOps.Microservice/StellaMicroserviceOptions.cs` 2. `StellaOps.Microservice/IStellaMicroserviceBuilder.cs` 3. `StellaOps.Microservice/StellaMicroserviceBuilder.cs` 4. `StellaOps.Microservice/IStellaMicroserviceHost.cs` 5. `StellaOps.Microservice/StellaMicroserviceHost.cs` 6. `StellaOps.Microservice/StellaMicroserviceExtensions.cs` 7. Builder pattern tests 8. Lifecycle tests (start/stop/reconnect) 9. Dual exposure mode tests --- ## Next Step Proceed to [Step 20: Endpoint Discovery & Registration](20-Step.md) to implement automatic endpoint discovery.