Files
git.stella-ops.org/docs/modules/router/aspnet-endpoint-bridge.md
StellaOps Bot 7503c19b8f Add determinism tests for verdict artifact generation and update SHA256 sums script
- 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.
2025-12-24 02:17:34 +02:00

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:

  1. HTTP Method: From HttpMethodMetadata
  2. Path: Normalize route pattern (see below)
  3. Authorization: From IAuthorizeData, IAllowAnonymous
  4. Parameters: From route pattern + parameter binding metadata
  5. Responses: From IProducesResponseTypeMetadata
  6. 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

  1. Discovery determinism: Same endpoints → same descriptor order
  2. Route normalization: Constraints stripped, paths normalized
  3. Authorization mapping: Policies → claims correctly
  4. Metadata extraction: All ASP.NET metadata captured

Integration Tests

  1. Minimal API dispatch: Route parameters, query, body binding
  2. Controller dispatch: Attribute routing, model binding
  3. Authorization flow: Claims checked, 403 on failure
  4. Filter execution: Endpoint filters run correctly
  5. Error mapping: Exceptions → correct status codes

End-to-End Tests

  1. HELLO registration: Bridge endpoints appear in Gateway
  2. Gateway routing: HTTP request → Router → ASP.NET → response
  3. 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])

  1. Remove [StellaEndpoint] handler classes
  2. Add AddStellaRouterBridge() configuration
  3. Add UseStellaRouterBridge() middleware
  4. Add/update microservice.yaml for claim overrides
  5. Remove duplicate endpoint registrations

  • 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