- Implemented comprehensive tests for verdict artifact generation to ensure deterministic outputs across various scenarios, including identical inputs, parallel execution, and change ordering. - Created helper methods for generating sample verdict inputs and computing canonical hashes. - Added tests to validate the stability of canonical hashes, proof spine ordering, and summary statistics. - Introduced a new PowerShell script to update SHA256 sums for files, ensuring accurate hash generation and file integrity checks.
31 KiB
Router · ASP.NET Endpoint Bridge
Status
- Version: 0.2 (revised)
- Last updated: 2025-12-24 (UTC)
- Implementation sprint:
SPRINT_8100_0011_0001
Problem Statement
StellaOps Router routes external HTTP requests (via Gateway) to internal microservices over binary transports (TCP/TLS/Messaging). Most StellaOps microservices are ASP.NET Core WebServices that register routes via standard ASP.NET endpoint registration (controllers or minimal APIs).
Current behavior: StellaOps.Microservice discovers and registers only Router-native endpoints implemented via [StellaEndpoint] handlers. If a service has only ASP.NET endpoints, it either:
- Registers zero Router endpoints (Gateway cannot route to it), or
- Duplicates routes as
[StellaEndpoint]handlers (route drift + duplicated security metadata).
Desired behavior: Treat ASP.NET endpoint registration as the single source of truth and automatically bridge it to Router endpoint registration.
Design Goals
| Goal | Description |
|---|---|
| Single source of truth | ASP.NET endpoint registration defines what the service exposes |
| Full ASP.NET fidelity | Authorization, filters, model binding, and all ASP.NET features work correctly |
| Determinism | Endpoint discovery produces stable ordering and normalized paths |
| Security alignment | Authorization metadata flows to Router RequiringClaims |
| Opt-in integration | Services explicitly enable the bridge via Program.cs |
| Offline-first | No runtime network dependency for discovery/dispatch |
Non-Goals (v0.1)
| Feature | Reason |
|---|---|
| SignalR/WebSocket support | Different protocol semantics |
| gRPC endpoint bridging | Different protocol |
| Streaming request bodies | Router SDK buffering limitation |
| Custom route constraints | Complexity; document as limitation |
| API versioning (header/query) | Complexity; use path-based versioning |
| Automatic schema generation | Depends on source generators; future enhancement |
Architecture Overview
┌─────────────────────────────────────────────────────────────────────┐
│ ASP.NET WebService │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Program.cs │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ builder.Services.AddStellaRouterBridge(options => { │ │ │
│ │ │ options.ServiceName = "scanner"; │ │ │
│ │ │ options.Version = "1.0.0"; │ │ │
│ │ │ }); │ │ │
│ │ │ app.UseStellaRouterBridge(); │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ StellaOps.Microservice.AspNetCore │ │
│ │ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ │
│ │ │ Discovery Provider │ │ Request Dispatcher │ │ │
│ │ │ ─────────────────── │ │ ─────────────────────────────── │ │ │
│ │ │ • EndpointDataSource│ │ • RequestFrame → HttpContext │ │ │
│ │ │ • Metadata extraction│ │ • ASP.NET pipeline execution │ │ │
│ │ │ • Route normalization│ │ • ResponseFrame capture │ │ │
│ │ │ • Auth claim mapping │ │ • DI scope management │ │ │
│ │ └──────────┬──────────┘ └──────────────┬──────────────────┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ Router SDK (StellaOps.Microservice) │ │ │
│ │ │ • HELLO registration with discovered endpoints │ │ │
│ │ │ • Request routing to dispatcher │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ ASP.NET Endpoints (unchanged) │ │
│ │ • Minimal APIs: app.MapGet("/api/...", handler) │ │
│ │ • Controllers: [ApiController] with [HttpGet], etc. │ │
│ │ • Route groups: app.MapGroup("/api").MapScannerEndpoints() │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
│ Router Transport (TCP/TLS/Messaging)
▼
┌───────────────────────────────┐
│ StellaOps.Gateway.WebService │
│ • HELLO processing │
│ • Endpoint routing │
│ • OpenAPI aggregation │
└───────────────────────────────┘
Component Design
1. StellaRouterBridgeOptions
Configuration for the bridge, specified in Program.cs:
public sealed class StellaRouterBridgeOptions
{
// === Required: Service Identity ===
public required string ServiceName { get; set; }
public required string Version { get; set; }
public required string Region { get; set; }
public string? InstanceId { get; set; } // Auto-generated if null
// === Authorization Mapping ===
public AuthorizationMappingStrategy AuthorizationMapping { get; set; }
= AuthorizationMappingStrategy.Hybrid;
public MissingAuthorizationBehavior OnMissingAuthorization { get; set; }
= MissingAuthorizationBehavior.RequireExplicit;
// === YAML Overrides ===
public string? YamlConfigPath { get; set; }
// === Metadata Extraction ===
public bool ExtractSchemas { get; set; } = true;
public bool ExtractOpenApiMetadata { get; set; } = true;
// === Route Handling ===
public UnsupportedConstraintBehavior OnUnsupportedConstraint { get; set; }
= UnsupportedConstraintBehavior.WarnAndStrip;
public Func<RouteEndpoint, bool>? EndpointFilter { get; set; }
// === Defaults ===
public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(30);
}
public enum AuthorizationMappingStrategy
{
YamlOnly, // Only use YAML overrides
AspNetMetadataOnly, // Only use ASP.NET metadata
Hybrid // ASP.NET + YAML (YAML wins on conflict)
}
public enum MissingAuthorizationBehavior
{
RequireExplicit, // Fail if no auth metadata
AllowAuthenticated, // Allow with empty claims (authenticated required)
WarnAndAllow // Log warning, allow with empty claims
}
public enum UnsupportedConstraintBehavior
{
Fail, // Fail discovery on unsupported constraint
WarnAndStrip, // Log warning, strip constraint
SilentStrip // Strip without warning
}
2. AspNetEndpointDescriptor
Extended endpoint descriptor with full ASP.NET metadata:
public sealed record AspNetEndpointDescriptor
{
// === Core Identity ===
public required string ServiceName { get; init; }
public required string Version { get; init; }
public required string Method { get; init; }
public required string Path { get; init; }
public TimeSpan DefaultTimeout { get; init; } = TimeSpan.FromSeconds(30);
public bool SupportsStreaming { get; init; }
// === Authorization ===
public IReadOnlyList<ClaimRequirement> RequiringClaims { get; init; } = [];
public IReadOnlyList<string> AuthorizationPolicies { get; init; } = [];
public IReadOnlyList<string> Roles { get; init; } = [];
public bool AllowAnonymous { get; init; }
public AuthorizationSource AuthorizationSource { get; init; }
// === Parameters ===
public IReadOnlyList<ParameterDescriptor> Parameters { get; init; } = [];
// === Responses ===
public IReadOnlyList<ResponseDescriptor> Responses { get; init; } = [];
// === OpenAPI ===
public string? OperationId { get; init; }
public string? Summary { get; init; }
public string? Description { get; init; }
public IReadOnlyList<string> Tags { get; init; } = [];
// === Schema ===
public EndpointSchemaInfo? SchemaInfo { get; init; }
// === Internal ===
internal RouteEndpoint? OriginalEndpoint { get; init; }
internal string? OriginalRoutePattern { get; init; }
/// <summary>
/// Convert to standard EndpointDescriptor for HELLO payload.
/// </summary>
public EndpointDescriptor ToEndpointDescriptor() => new()
{
ServiceName = ServiceName,
Version = Version,
Method = Method,
Path = Path,
DefaultTimeout = DefaultTimeout,
SupportsStreaming = SupportsStreaming,
RequiringClaims = RequiringClaims,
SchemaInfo = SchemaInfo
};
}
public sealed record ParameterDescriptor
{
public required string Name { get; init; }
public required ParameterSource Source { get; init; }
public required Type Type { get; init; }
public bool IsRequired { get; init; } = true;
public object? DefaultValue { get; init; }
public string? Description { get; init; }
}
public enum ParameterSource { Route, Query, Header, Body, Services }
public sealed record ResponseDescriptor
{
public required int StatusCode { get; init; }
public Type? ResponseType { get; init; }
public string? Description { get; init; }
public string? ContentType { get; init; } = "application/json";
}
public enum AuthorizationSource { None, AspNetMetadata, YamlOverride, Hybrid }
3. IAspNetEndpointDiscoveryProvider
Discovery provider interface:
public interface IAspNetEndpointDiscoveryProvider : IEndpointDiscoveryProvider
{
/// <summary>
/// Discover ASP.NET endpoints with full metadata.
/// </summary>
IReadOnlyList<AspNetEndpointDescriptor> DiscoverAspNetEndpoints();
}
4. IAuthorizationClaimMapper
Authorization-to-claims mapping interface:
public interface IAuthorizationClaimMapper
{
/// <summary>
/// Map ASP.NET authorization metadata to Router claim requirements.
/// </summary>
Task<AuthorizationMappingResult> MapAsync(
RouteEndpoint endpoint,
CancellationToken cancellationToken = default);
}
public sealed record AuthorizationMappingResult
{
public IReadOnlyList<ClaimRequirement> Claims { get; init; } = [];
public IReadOnlyList<string> Policies { get; init; } = [];
public IReadOnlyList<string> Roles { get; init; } = [];
public bool AllowAnonymous { get; init; }
public AuthorizationSource Source { get; init; }
}
5. IAspNetRouterRequestDispatcher
Request dispatch interface:
public interface IAspNetRouterRequestDispatcher
{
/// <summary>
/// Dispatch a Router request frame through ASP.NET pipeline.
/// </summary>
Task<ResponseFrame> DispatchAsync(
RequestFrame request,
CancellationToken cancellationToken = default);
}
Endpoint Discovery Algorithm
Step 1: Enumerate Endpoints
var endpoints = endpointDataSource.Endpoints
.OfType<RouteEndpoint>()
.Where(e => e.Metadata.GetMetadata<HttpMethodMetadata>() is not null)
.Where(options.EndpointFilter ?? (_ => true));
Step 2: Extract Metadata per Endpoint
For each RouteEndpoint:
- HTTP Method: From
HttpMethodMetadata - Path: Normalize route pattern (see below)
- Authorization: From
IAuthorizeData,IAllowAnonymous - Parameters: From route pattern + parameter binding metadata
- Responses: From
IProducesResponseTypeMetadata - OpenAPI: From
IEndpointNameMetadata,IEndpointSummaryMetadata,ITagsMetadata
Step 3: Normalize Route Pattern
public static string NormalizeRoutePattern(RoutePattern pattern)
{
var raw = pattern.RawText ?? BuildFromSegments(pattern);
// 1. Ensure leading slash
if (!raw.StartsWith('/'))
raw = "/" + raw;
// 2. Strip constraints: {id:int} → {id}
raw = Regex.Replace(raw, @"\{(\*?)([A-Za-z0-9_]+)(:[^}]+)?\}", "{$2}");
// 3. Normalize catch-all: {**path} → {path}
raw = raw.Replace("**", "", StringComparison.Ordinal);
// 4. Remove trailing slash
raw = raw.TrimEnd('/');
// 5. Empty path becomes "/"
return string.IsNullOrEmpty(raw) ? "/" : raw;
}
Step 4: Deterministic Ordering
Sort endpoints for stable HELLO payloads:
var ordered = endpoints
.OrderBy(e => e.Path, StringComparer.OrdinalIgnoreCase)
.ThenBy(e => GetMethodOrder(e.Method))
.ThenBy(e => e.OriginalEndpoint?.DisplayName ?? "");
static int GetMethodOrder(string method) => method.ToUpperInvariant() switch
{
"GET" => 0,
"POST" => 1,
"PUT" => 2,
"PATCH" => 3,
"DELETE" => 4,
"OPTIONS" => 5,
"HEAD" => 6,
_ => 7
};
Authorization Mapping
Mapping Rules
| ASP.NET Metadata | Router Mapping |
|---|---|
[Authorize] (no args) |
Empty RequiringClaims (authenticated required) |
[Authorize(Policy = "X")] |
Resolve policy → claims via IAuthorizationPolicyProvider |
[Authorize(Roles = "A,B")] |
ClaimRequirement(ClaimTypes.Role, "A"), ClaimRequirement(ClaimTypes.Role, "B") |
[AllowAnonymous] |
AllowAnonymous = true, empty claims |
.RequireAuthorization("Policy") |
Same as [Authorize(Policy)] |
Policy Resolution
public async Task<IReadOnlyList<ClaimRequirement>> ResolvePolicyAsync(
string policyName,
IAuthorizationPolicyProvider policyProvider)
{
var policy = await policyProvider.GetPolicyAsync(policyName);
if (policy is null)
return [];
var claims = new List<ClaimRequirement>();
foreach (var requirement in policy.Requirements)
{
switch (requirement)
{
case ClaimsAuthorizationRequirement claimsReq:
foreach (var value in claimsReq.AllowedValues ?? [null])
{
claims.Add(new ClaimRequirement
{
Type = claimsReq.ClaimType,
Value = value
});
}
break;
case RolesAuthorizationRequirement rolesReq:
foreach (var role in rolesReq.AllowedRoles)
{
claims.Add(new ClaimRequirement
{
Type = ClaimTypes.Role,
Value = role
});
}
break;
// Other requirement types: log warning, continue
}
}
return claims;
}
YAML Override Merge
When AuthorizationMappingStrategy.Hybrid:
public IReadOnlyList<ClaimRequirement> MergeWithYaml(
IReadOnlyList<ClaimRequirement> aspNetClaims,
EndpointOverrideConfig? yamlOverride)
{
if (yamlOverride?.RequiringClaims is not { Count: > 0 } yamlClaims)
return aspNetClaims;
// YAML completely replaces ASP.NET claims when specified
return yamlClaims;
}
Request Dispatch
Dispatch Flow
RequestFrame (from Router)
│
▼
┌───────────────────────────────────────┐
│ 1. Create DI Scope │
│ var scope = CreateAsyncScope() │
└───────────────────────────────────────┘
│
▼
┌───────────────────────────────────────┐
│ 2. Build HttpContext │
│ • Method, Path, QueryString │
│ • Headers (including identity) │
│ • Body stream │
│ • RequestServices = scope.Provider │
│ • CancellationToken wiring │
└───────────────────────────────────────┘
│
▼
┌───────────────────────────────────────┐
│ 3. Match Endpoint │
│ Use ASP.NET EndpointSelector │
│ Preserves constraints/precedence │
└───────────────────────────────────────┘
│
▼
┌───────────────────────────────────────┐
│ 4. Populate Identity │
│ Map X-StellaOps-* headers to │
│ ClaimsPrincipal on HttpContext │
└───────────────────────────────────────┘
│
▼
┌───────────────────────────────────────┐
│ 5. Execute RequestDelegate │
│ Runs full ASP.NET pipeline: │
│ • Endpoint filters │
│ • Authorization filters │
│ • Model binding │
│ • Handler execution │
└───────────────────────────────────────┘
│
▼
┌───────────────────────────────────────┐
│ 6. Capture Response │
│ • Status code │
│ • Headers (filtered) │
│ • Body bytes (buffered) │
└───────────────────────────────────────┘
│
▼
┌───────────────────────────────────────┐
│ 7. Dispose Scope │
│ await scope.DisposeAsync() │
└───────────────────────────────────────┘
│
▼
ResponseFrame (to Router)
HttpContext Construction
public async Task<ResponseFrame> DispatchAsync(
RequestFrame request,
CancellationToken cancellationToken)
{
await using var scope = _serviceProvider.CreateAsyncScope();
var httpContext = new DefaultHttpContext
{
RequestServices = scope.ServiceProvider
};
// Link cancellation
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
httpContext.RequestAborted);
httpContext.RequestAborted = linkedCts.Token;
// Populate request
var httpRequest = httpContext.Request;
httpRequest.Method = request.Method;
(httpRequest.Path, httpRequest.QueryString) = ParsePathAndQuery(request.Path);
foreach (var (key, value) in request.Headers)
{
httpRequest.Headers[key] = value;
}
if (request.Body is { Length: > 0 })
{
httpRequest.Body = new MemoryStream(request.Body);
httpRequest.ContentLength = request.Body.Length;
}
// Set trace identifier
httpContext.TraceIdentifier = request.CorrelationId;
// Populate identity from headers
PopulateIdentity(httpContext, request.Headers);
// Match and execute endpoint
var endpoint = await MatchEndpointAsync(httpContext);
if (endpoint is null)
{
return CreateNotFoundResponse(request.CorrelationId);
}
httpContext.SetEndpoint(endpoint);
// Capture response
var responseBody = new MemoryStream();
httpContext.Response.Body = responseBody;
try
{
await endpoint.RequestDelegate!(httpContext);
}
catch (Exception ex)
{
return CreateErrorResponse(request.CorrelationId, ex);
}
// Build response frame
return new ResponseFrame
{
CorrelationId = request.CorrelationId,
StatusCode = httpContext.Response.StatusCode,
Headers = CaptureResponseHeaders(httpContext.Response),
Body = responseBody.ToArray()
};
}
Identity Population
Map Gateway-provided identity headers to ClaimsPrincipal:
private void PopulateIdentity(HttpContext httpContext, IReadOnlyDictionary<string, string> headers)
{
var claims = new List<Claim>();
if (headers.TryGetValue("X-StellaOps-Actor", out var actor) && !string.IsNullOrEmpty(actor))
{
claims.Add(new Claim(ClaimTypes.NameIdentifier, actor));
claims.Add(new Claim(StellaOpsClaimTypes.Subject, actor));
}
if (headers.TryGetValue("X-StellaOps-Tenant", out var tenant) && !string.IsNullOrEmpty(tenant))
{
claims.Add(new Claim(StellaOpsClaimTypes.Tenant, tenant));
}
if (headers.TryGetValue("X-StellaOps-Scopes", out var scopes) && !string.IsNullOrEmpty(scopes))
{
foreach (var scope in scopes.Split(' ', StringSplitOptions.RemoveEmptyEntries))
{
claims.Add(new Claim(StellaOpsClaimTypes.ScopeItem, scope));
}
}
if (claims.Count > 0)
{
var identity = new ClaimsIdentity(claims, "StellaRouter");
httpContext.User = new ClaimsPrincipal(identity);
}
}
Program.cs Integration
Service Registration
public static class StellaRouterBridgeExtensions
{
public static IServiceCollection AddStellaRouterBridge(
this IServiceCollection services,
Action<StellaRouterBridgeOptions> configure)
{
var options = new StellaRouterBridgeOptions
{
ServiceName = "",
Version = "",
Region = ""
};
configure(options);
ValidateOptions(options);
services.AddSingleton(options);
services.AddSingleton<IAuthorizationClaimMapper, DefaultAuthorizationClaimMapper>();
services.AddSingleton<IAspNetEndpointDiscoveryProvider, AspNetCoreEndpointDiscoveryProvider>();
services.AddSingleton<IAspNetRouterRequestDispatcher, AspNetRouterRequestDispatcher>();
// Register as IEndpointDiscoveryProvider for Router SDK integration
services.AddSingleton<IEndpointDiscoveryProvider>(sp =>
sp.GetRequiredService<IAspNetEndpointDiscoveryProvider>());
// Wire into Router SDK
services.AddStellaMicroservice(microserviceOptions =>
{
microserviceOptions.ServiceName = options.ServiceName;
microserviceOptions.Version = options.Version;
microserviceOptions.Region = options.Region;
microserviceOptions.InstanceId = options.InstanceId ?? Guid.NewGuid().ToString();
});
return services;
}
public static IApplicationBuilder UseStellaRouterBridge(this IApplicationBuilder app)
{
// Ensure EndpointDataSource is available (after UseRouting)
var endpointDataSource = app.ApplicationServices
.GetService<EndpointDataSource>()
?? throw new InvalidOperationException(
"UseStellaRouterBridge must be called after UseRouting()");
// Discovery happens on first Router HELLO
// Dispatch is handled by Router SDK
return app;
}
}
YAML Override Format
The existing microservice.yaml format is extended:
microservice:
serviceName: scanner
version: "1.0.0"
region: "${REGION:default}"
endpoints:
# Override by method + path
- method: POST
path: /api/reports
timeoutSeconds: 60
supportsStreaming: false
requiringClaims:
- type: "scanner.reports.read"
# Replaces any ASP.NET-derived claims for this endpoint
# Endpoint with no authorization (explicitly allow authenticated)
- method: GET
path: /api/health
requiringClaims: [] # Empty = authenticated only, no specific claims
# Override specific claim type mapping
- method: DELETE
path: /api/scans/{id}
requiringClaims:
- type: "role"
value: "scanner-admin"
- type: "scanner.scans.delete"
ASP.NET Feature Support Matrix
Fully Supported
| Feature | Discovery | Dispatch | Notes |
|---|---|---|---|
Minimal APIs (MapGet, etc.) |
✓ | ✓ | Primary use case |
Controllers ([ApiController]) |
✓ | ✓ | Full support |
Route groups (MapGroup) |
✓ | ✓ | Path composition |
[Authorize] attribute |
✓ | ✓ | Claims extraction |
[AllowAnonymous] |
✓ | ✓ | Explicit anonymous |
.RequireAuthorization() |
✓ | ✓ | Policy resolution |
[FromBody] binding |
✓ (type) | ✓ | JSON deserialization |
[FromRoute] binding |
✓ | ✓ | Path parameters |
[FromQuery] binding |
✓ | ✓ | Query parameters |
[FromHeader] binding |
✓ | ✓ | Header values |
[FromServices] injection |
N/A | ✓ | DI resolution |
.Produces<T>() |
✓ | N/A | Schema metadata |
.WithName() / .WithSummary() |
✓ | N/A | OpenAPI metadata |
.WithTags() |
✓ | N/A | Grouping |
| Endpoint filters | N/A | ✓ | Filter pipeline |
CancellationToken |
N/A | ✓ | From Router frame |
Route constraints ({id:int}) |
✓ (stripped) | ✓ | ASP.NET matcher |
Catch-all routes ({**path}) |
✓ | ✓ | Normalized |
Not Supported (v0.1)
| Feature | Reason | Workaround |
|---|---|---|
| SignalR hubs | Different protocol | Use native ASP.NET |
| gRPC services | Different protocol | Use native gRPC |
| Streaming request bodies | SDK limitation | Use IRawStellaEndpoint |
| Custom constraints | Complexity | Use standard constraints |
| API versioning (header/query) | Complexity | Path-based versioning |
IFormFile uploads |
Not buffered | Use raw endpoint |
Error Handling
Discovery Errors
| Condition | Behavior | Configuration |
|---|---|---|
| No authorization metadata | Fail discovery | OnMissingAuthorization = RequireExplicit |
| Unsupported constraint | Log warning, strip | OnUnsupportedConstraint = WarnAndStrip |
| Duplicate endpoints | Log warning, keep first | Always |
| Invalid route pattern | Skip endpoint, log error | Always |
Dispatch Errors
| Condition | Response |
|---|---|
| No matching endpoint | 404 Not Found |
| Authorization failure | 403 Forbidden |
| Model binding failure | 400 Bad Request |
| Handler exception | 500 Internal Server Error |
| Cancellation | No response (connection closed) |
Testing Strategy
Unit Tests
- Discovery determinism: Same endpoints → same descriptor order
- Route normalization: Constraints stripped, paths normalized
- Authorization mapping: Policies → claims correctly
- Metadata extraction: All ASP.NET metadata captured
Integration Tests
- Minimal API dispatch: Route parameters, query, body binding
- Controller dispatch: Attribute routing, model binding
- Authorization flow: Claims checked, 403 on failure
- Filter execution: Endpoint filters run correctly
- Error mapping: Exceptions → correct status codes
End-to-End Tests
- HELLO registration: Bridge endpoints appear in Gateway
- Gateway routing: HTTP request → Router → ASP.NET → response
- OpenAPI aggregation: Bridged endpoints in Gateway OpenAPI
Migration Guide
From HTTP-Only Service
// Before: HTTP only
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
await app.RunAsync();
// After: HTTP + Router bridge
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddStellaRouterBridge(options =>
{
options.ServiceName = "myservice";
options.Version = "1.0.0";
options.Region = "default";
});
builder.Services.AddMessagingTransportClient(); // Add transport
var app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseStellaRouterBridge(); // Enable bridge
app.MapControllers();
await app.RunAsync();
From Dual Registration (HTTP + [StellaEndpoint])
- Remove
[StellaEndpoint]handler classes - Add
AddStellaRouterBridge()configuration - Add
UseStellaRouterBridge()middleware - Add/update
microservice.yamlfor claim overrides - Remove duplicate endpoint registrations
Related Documents
- Router architecture:
docs/modules/router/architecture.md - Migration guide:
docs/modules/router/migration-guide.md - Gateway identity policy:
docs/modules/gateway/identity-header-policy.md - Implementation sprint:
docs/implplan/SPRINT_8100_0011_0001_router_sdk_aspnet_bridge.md