Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
- Implemented MigrationCategoryTests to validate migration categorization for startup, release, seed, and data migrations. - Added tests for edge cases, including null, empty, and whitespace migration names. - Created StartupMigrationHostTests to verify the behavior of the migration host with real PostgreSQL instances using Testcontainers. - Included tests for migration execution, schema creation, and handling of pending release migrations. - Added SQL migration files for testing: creating a test table, adding a column, a release migration, and seeding data.
715 lines
24 KiB
Markdown
715 lines
24 KiB
Markdown
# 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
|
|
{
|
|
/// <summary>Service name for registration.</summary>
|
|
public required string ServiceName { get; set; }
|
|
|
|
/// <summary>Unique instance identifier (auto-generated if not set).</summary>
|
|
public string InstanceId { get; set; } = Guid.NewGuid().ToString("N")[..8];
|
|
|
|
/// <summary>Service version for routing.</summary>
|
|
public string Version { get; set; } = "1.0.0";
|
|
|
|
/// <summary>Region for routing affinity.</summary>
|
|
public string? Region { get; set; }
|
|
|
|
/// <summary>Tags for routing metadata.</summary>
|
|
public Dictionary<string, string> Tags { get; set; } = new();
|
|
|
|
/// <summary>Router connection pool.</summary>
|
|
public List<RouterConnectionConfig> Routers { get; set; } = new();
|
|
|
|
/// <summary>Transport configuration.</summary>
|
|
public TransportConfig Transport { get; set; } = new();
|
|
|
|
/// <summary>Endpoint discovery configuration.</summary>
|
|
public EndpointDiscoveryConfig Discovery { get; set; } = new();
|
|
|
|
/// <summary>Heartbeat configuration.</summary>
|
|
public HeartbeatConfig Heartbeat { get; set; } = new();
|
|
|
|
/// <summary>Dual exposure mode configuration.</summary>
|
|
public DualExposureConfig? DualExposure { get; set; }
|
|
|
|
/// <summary>Graceful shutdown timeout.</summary>
|
|
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
|
|
{
|
|
/// <summary>Assemblies to scan for endpoints.</summary>
|
|
public List<string> ScanAssemblies { get; set; } = new();
|
|
|
|
/// <summary>Path to YAML overrides file.</summary>
|
|
public string? ConfigFilePath { get; set; }
|
|
|
|
/// <summary>Base path prefix for all endpoints.</summary>
|
|
public string? BasePath { get; set; }
|
|
|
|
/// <summary>Whether to auto-discover endpoints via reflection.</summary>
|
|
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
|
|
{
|
|
/// <summary>Enable direct HTTP access.</summary>
|
|
public bool Enabled { get; set; } = false;
|
|
|
|
/// <summary>HTTP port for direct access.</summary>
|
|
public int HttpPort { get; set; } = 8080;
|
|
|
|
/// <summary>Default claims for direct access (no JWT).</summary>
|
|
public Dictionary<string, string> DefaultClaims { get; set; } = new();
|
|
|
|
/// <summary>Whether to require JWT for direct access.</summary>
|
|
public bool RequireAuthentication { get; set; } = false;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Host Builder Implementation
|
|
|
|
```csharp
|
|
namespace StellaOps.Microservice;
|
|
|
|
public interface IStellaMicroserviceBuilder
|
|
{
|
|
IStellaMicroserviceBuilder ConfigureServices(Action<IServiceCollection> configure);
|
|
IStellaMicroserviceBuilder ConfigureTransport(Action<TransportConfig> configure);
|
|
IStellaMicroserviceBuilder ConfigureEndpoints(Action<EndpointDiscoveryConfig> configure);
|
|
IStellaMicroserviceBuilder AddRouter(string host, int port, string transport = "TCP");
|
|
IStellaMicroserviceBuilder EnableDualExposure(Action<DualExposureConfig>? configure = null);
|
|
IStellaMicroserviceBuilder UseYamlConfig(string path);
|
|
IStellaMicroserviceHost Build();
|
|
}
|
|
|
|
public sealed class StellaMicroserviceBuilder : IStellaMicroserviceBuilder
|
|
{
|
|
private readonly StellaMicroserviceOptions _options;
|
|
private readonly IServiceCollection _services;
|
|
private readonly List<Action<IServiceCollection>> _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<IServiceCollection> configure)
|
|
{
|
|
_configureActions.Add(configure);
|
|
return this;
|
|
}
|
|
|
|
public IStellaMicroserviceBuilder ConfigureTransport(Action<TransportConfig> configure)
|
|
{
|
|
configure(_options.Transport);
|
|
return this;
|
|
}
|
|
|
|
public IStellaMicroserviceBuilder ConfigureEndpoints(Action<EndpointDiscoveryConfig> 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<DualExposureConfig>? 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<IStellaMicroserviceHost>();
|
|
}
|
|
|
|
private void AddCoreServices()
|
|
{
|
|
_services.AddSingleton<IStellaMicroserviceHost, StellaMicroserviceHost>();
|
|
_services.AddSingleton<IEndpointRegistry, EndpointRegistry>();
|
|
_services.AddSingleton<IRequestDispatcher, RequestDispatcher>();
|
|
_services.AddSingleton<IPayloadSerializer, MessagePackPayloadSerializer>();
|
|
}
|
|
|
|
private void AddTransportServices()
|
|
{
|
|
_services.AddSingleton<TcpFrameCodec>();
|
|
|
|
switch (_options.Transport.Default.ToUpper())
|
|
{
|
|
case "TCP":
|
|
_services.AddSingleton<ITransportServer, TcpTransportClient>();
|
|
break;
|
|
case "TLS":
|
|
_services.AddSingleton<ICertificateProvider, CertificateProvider>();
|
|
_services.AddSingleton<ITransportServer, TlsTransportClient>();
|
|
break;
|
|
case "INMEMORY":
|
|
// InMemory requires hub to be provided externally
|
|
_services.AddSingleton<ITransportServer, InMemoryTransportServer>();
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void AddEndpointServices()
|
|
{
|
|
_services.AddSingleton<IEndpointDiscovery, ReflectionEndpointDiscovery>();
|
|
|
|
if (!string.IsNullOrEmpty(_options.Discovery.ConfigFilePath))
|
|
{
|
|
_services.AddSingleton<IEndpointOverrideProvider, YamlEndpointOverrideProvider>();
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 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<StellaMicroserviceHost> _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<StellaMicroserviceHost> 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<ResponsePayload> 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<string, string>(),
|
|
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
|
|
{
|
|
/// <summary>
|
|
/// Adds Stella microservice to an existing ASP.NET Core host.
|
|
/// </summary>
|
|
public static IServiceCollection AddStellaMicroservice(
|
|
this IServiceCollection services,
|
|
Action<StellaMicroserviceOptions> configure)
|
|
{
|
|
var options = new StellaMicroserviceOptions { ServiceName = "unknown" };
|
|
configure(options);
|
|
|
|
services.AddSingleton(options);
|
|
services.AddSingleton<IEndpointRegistry, EndpointRegistry>();
|
|
services.AddSingleton<IRequestDispatcher, RequestDispatcher>();
|
|
services.AddSingleton<IPayloadSerializer, MessagePackPayloadSerializer>();
|
|
services.AddSingleton<TcpFrameCodec>();
|
|
|
|
// Add transport based on configuration
|
|
switch (options.Transport.Default.ToUpper())
|
|
{
|
|
case "TCP":
|
|
services.AddSingleton<ITransportServer, TcpTransportClient>();
|
|
break;
|
|
case "TLS":
|
|
services.AddSingleton<ICertificateProvider, CertificateProvider>();
|
|
services.AddSingleton<ITransportServer, TlsTransportClient>();
|
|
break;
|
|
}
|
|
|
|
services.AddSingleton<IStellaMicroserviceHost, StellaMicroserviceHost>();
|
|
services.AddHostedService(sp => (StellaMicroserviceHost)sp.GetRequiredService<IStellaMicroserviceHost>());
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configures an endpoint handler for the microservice.
|
|
/// </summary>
|
|
public static IServiceCollection AddEndpointHandler<THandler>(
|
|
this IServiceCollection services)
|
|
where THandler : class, IEndpointHandler
|
|
{
|
|
services.AddScoped<IEndpointHandler, THandler>();
|
|
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<BillingContext>();
|
|
services.AddScoped<InvoiceHandler>();
|
|
})
|
|
.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<string, string>
|
|
{
|
|
["tier"] = "free"
|
|
}
|
|
};
|
|
});
|
|
|
|
builder.Services.AddEndpointHandler<UserEndpointHandler>();
|
|
|
|
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.
|