Router · ASP.NET Endpoint Bridge

Status


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:

Desired behavior: Treat ASP.NET endpoint registration as the single source of truth and automatically bridge it to Router endpoint registration.


Design Goals

GoalDescription
Single source of truthASP.NET endpoint registration defines what the service exposes
Full ASP.NET fidelityAuthorization, filters, model binding, and all ASP.NET features work correctly
DeterminismEndpoint discovery produces stable ordering and normalized paths
Security alignmentAuthorization metadata flows to Router RequiringClaims
Opt-in integrationServices explicitly enable the bridge via Program.cs
Offline-firstNo runtime network dependency for discovery/dispatch

Non-Goals (v0.1)

FeatureReason
SignalR/WebSocket supportDifferent protocol semantics
gRPC endpoint bridgingDifferent protocol
Streaming request bodiesRouter SDK buffering limitation
Custom route constraintsComplexity; document as limitation
API versioning (header/query)Complexity; use path-based versioning
Automatic schema generationDepends 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 MetadataRouter 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-TenantId", 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

FeatureDiscoveryDispatchNotes
Minimal APIs (MapGet, etc.)Primary use case
Controllers ([ApiController])Full support
Route groups (MapGroup)Path composition
[Authorize] attributeClaims extraction
[AllowAnonymous]Explicit anonymous
.RequireAuthorization()Policy resolution
[FromBody] binding✓ (type)JSON deserialization
[FromRoute] bindingPath parameters
[FromQuery] bindingQuery parameters
[FromHeader] bindingHeader values
[FromServices] injectionN/ADI resolution
.Produces<T>()N/ASchema metadata
.WithName() / .WithSummary()N/AOpenAPI metadata
.WithTags()N/AGrouping
Endpoint filtersN/AFilter pipeline
CancellationTokenN/AFrom Router frame
Route constraints ({id:int})✓ (stripped)ASP.NET matcher
Catch-all routes ({**path})Normalized

Not Supported (v0.1)

FeatureReasonWorkaround
SignalR hubsDifferent protocolUse native ASP.NET
gRPC servicesDifferent protocolUse native gRPC
Streaming request bodiesSDK limitationUse IRawStellaEndpoint
Custom constraintsComplexityUse standard constraints
API versioning (header/query)ComplexityPath-based versioning
IFormFile uploadsNot bufferedUse raw endpoint

Error Handling

Discovery Errors

ConditionBehaviorConfiguration
No authorization metadataFail discoveryOnMissingAuthorization = RequireExplicit
Unsupported constraintLog warning, stripOnUnsupportedConstraint = WarnAndStrip
Duplicate endpointsLog warning, keep firstAlways
Invalid route patternSkip endpoint, log errorAlways

Dispatch Errors

ConditionResponse
No matching endpoint404 Not Found
Authorization failure403 Forbidden
Model binding failure400 Bad Request
Handler exception500 Internal Server Error
CancellationNo response (connection closed)

Troubleshooting

Common Issues

1. Endpoints Not Discovered

Symptom: Gateway shows 0 endpoints for service, or specific endpoints missing.

Causes & Solutions:

CauseSolution
UseStellaRouterBridge() called before MapControllers()Call UseStellaRouterBridge() after all endpoint registration
Route filtered by EndpointFilterCheck filter logic, ensure endpoint matches
Route filtered by IncludePathPatternsVerify path pattern includes the endpoint
Route filtered by ExcludePathPatternsVerify endpoint isn’t accidentally excluded
Missing [Authorize] with RequireExplicitAdd authorization or change MissingAuthorizationBehavior

Debug:

// Enable discovery logging
builder.Logging.AddFilter("StellaOps.Microservice.AspNetCore", LogLevel.Debug);

2. Authorization Claims Not Extracted

Symptom: Endpoints registered but RequiringClaims is empty when it shouldn’t be.

Causes & Solutions:

CauseSolution
[Authorize] without policy/rolesAdd explicit policy or roles
Policy not registeredRegister policy with AddAuthorization()
AuthorizationMappingStrategy.YamlOnly setUse Hybrid or AspNetMetadataOnly
Custom policy doesn’t have claim requirementsUse claims-based policies

Debug:

// Log authorization mapping
var mapper = app.Services.GetRequiredService<IAuthorizationClaimMapper>();
var result = await mapper.MapAsync(endpoint);
Console.WriteLine($"Claims: {string.Join(", ", result.Claims)}");

3. Request Dispatch Fails with 404

Symptom: Gateway routes request but microservice returns 404.

Causes & Solutions:

CauseSolution
Path parameters not matchedVerify parameter names in route pattern
Method mismatchVerify HTTP method matches endpoint
Route constraint rejected valueUse standard constraints that bridge supports
Catch-all route not handledEnsure {**path} is normalized correctly

Debug:

# Check registered endpoints
curl http://localhost:5000/.well-known/stella-endpoints

4. Model Binding Errors

Symptom: Requests return 400 Bad Request with binding errors.

Causes & Solutions:

CauseSolution
[FromBody] type mismatchVerify request body matches expected type
Required parameter missingInclude all [FromRoute]/[FromQuery] parameters
[FromHeader] not populatedHeaders forwarded via X-StellaOps-Header-*
Complex type not deserializedVerify JSON serialization settings

Debug:

// Enable model binding logging
builder.Logging.AddFilter("Microsoft.AspNetCore.Mvc.ModelBinding", LogLevel.Debug);

5. Identity Not Populated

Symptom: User.Identity is null or claims missing in endpoint handler.

Causes & Solutions:

CauseSolution
Gateway not forwarding identityVerify Gateway identity-header-policy configured
Missing X-StellaOps-UserId headerGateway must send identity headers
UseAuthentication() not calledCall before UseStellaRouterBridge()
Custom claims not mappedUse ClaimsPrincipalBuilder for custom claims

Debug:

app.MapGet("/debug/identity", (HttpContext ctx) =>
    new {
        IsAuthenticated = ctx.User.Identity?.IsAuthenticated,
        Name = ctx.User.Identity?.Name,
        Claims = ctx.User.Claims.Select(c => new { c.Type, c.Value })
    });

6. Performance Issues

Symptom: High latency or memory usage.

Causes & Solutions:

CauseSolution
HttpContext allocation per requestPool contexts (internal implementation)
Large request bodies bufferedUse streaming endpoints for large payloads
Discovery runs too frequentlyDiscovery runs once at startup; cache is stable
Many endpoints slow startupDiscovery is O(n) but runs once

Metrics to monitor:

7. YAML Override Not Applied

Symptom: Claims from YAML file not appearing in endpoint registration.

Causes & Solutions:

CauseSolution
YamlConfigPath not setSet options.YamlConfigPath = "router.yaml"
File not foundUse absolute path or verify relative path
Path pattern doesn’t matchYAML paths are case-insensitive, verify pattern
AuthorizationMappingStrategy.AspNetMetadataOnlyUse YamlOnly or Hybrid

Example YAML:

# router.yaml
endpoints:
  - path: "/api/admin/**"
    requiringClaims:
      - type: "Role"
        value: "admin"

Diagnostic Endpoints

The bridge adds optional diagnostic endpoints (development only):

EndpointDescription
/.well-known/stella-endpointsLists all discovered endpoints
/.well-known/stella-bridge-statusShows bridge configuration and health

Enable in development:

builder.Services.AddStellaRouterBridge(options =>
{
    options.EnableDiagnosticEndpoints = builder.Environment.IsDevelopment();
    // ...
});

Logging Categories

Configure logging for troubleshooting:

{
  "Logging": {
    "LogLevel": {
      "StellaOps.Microservice.AspNetCore.Discovery": "Debug",
      "StellaOps.Microservice.AspNetCore.Authorization": "Debug",
      "StellaOps.Microservice.AspNetCore.Dispatch": "Information"
    }
  }
}

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