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.
This commit is contained in:
@@ -56,6 +56,7 @@ The Router replaces the Serdica HTTP-to-RabbitMQ pattern with a simpler, generic
|
||||
| TLS Transport | `StellaOps.Router.Transport.Tls` | Encrypted TCP transport |
|
||||
| UDP Transport | `StellaOps.Router.Transport.Udp` | Small payload transport |
|
||||
| RabbitMQ Transport | `StellaOps.Router.Transport.RabbitMQ` | Message queue transport |
|
||||
| Messaging Transport | `StellaOps.Router.Transport.Messaging` | Messaging/RPC transport (Valkey-backed via `StellaOps.Messaging.Transport.Valkey`) |
|
||||
|
||||
## Solution Structure
|
||||
|
||||
@@ -86,6 +87,8 @@ StellaOps.Router.slnx
|
||||
| [openapi-aggregation.md](openapi-aggregation.md) | OpenAPI document generation |
|
||||
| [migration-guide.md](migration-guide.md) | WebService to Microservice migration |
|
||||
| [rate-limiting.md](rate-limiting.md) | Centralized router rate limiting |
|
||||
| [aspnet-endpoint-bridge.md](aspnet-endpoint-bridge.md) | Using ASP.NET endpoint registration as Router endpoint registration |
|
||||
| [messaging-valkey-transport.md](messaging-valkey-transport.md) | Messaging transport over Valkey |
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
857
docs/modules/router/aspnet-endpoint-bridge.md
Normal file
857
docs/modules/router/aspnet-endpoint-bridge.md
Normal file
@@ -0,0 +1,857 @@
|
||||
# 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`:
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
public interface IAspNetEndpointDiscoveryProvider : IEndpointDiscoveryProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Discover ASP.NET endpoints with full metadata.
|
||||
/// </summary>
|
||||
IReadOnlyList<AspNetEndpointDescriptor> DiscoverAspNetEndpoints();
|
||||
}
|
||||
```
|
||||
|
||||
### 4. IAuthorizationClaimMapper
|
||||
|
||||
Authorization-to-claims mapping interface:
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
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`:
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
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`:
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```yaml
|
||||
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
|
||||
|
||||
```csharp
|
||||
// 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
|
||||
|
||||
---
|
||||
|
||||
## 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`
|
||||
75
docs/modules/router/messaging-valkey-transport.md
Normal file
75
docs/modules/router/messaging-valkey-transport.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Router · Messaging Transport over Valkey (Draft v0.1)
|
||||
|
||||
## Status
|
||||
- Draft; intended for implementation via a dedicated sprint.
|
||||
- Last updated: 2025-12-23 (UTC).
|
||||
|
||||
## Purpose
|
||||
Enable Gateway ↔ microservice Router traffic over an offline-friendly, Redis-compatible transport (Valkey) by using the existing **Messaging** transport layer:
|
||||
- Router transport: `StellaOps.Router.Transport.Messaging`
|
||||
- Messaging backend: `StellaOps.Messaging.Transport.Valkey`
|
||||
|
||||
This supports environments where direct TCP/TLS microservice connections are undesirable, and where an internal message bus is the preferred control plane.
|
||||
|
||||
## What Exists Today (Repository Reality)
|
||||
- Messaging transport server/client:
|
||||
- `src/__Libraries/StellaOps.Router.Transport.Messaging/MessagingTransportServer.cs`
|
||||
- `src/__Libraries/StellaOps.Router.Transport.Messaging/MessagingTransportClient.cs`
|
||||
- Valkey-backed message queue factory:
|
||||
- `src/__Libraries/StellaOps.Messaging.Transport.Valkey/ValkeyMessageQueueFactory.cs`
|
||||
- Gateway WebService currently starts only TCP/TLS transport servers:
|
||||
- `src/Gateway/StellaOps.Gateway.WebService/Program.cs`
|
||||
- `src/Gateway/StellaOps.Gateway.WebService/Services/GatewayHostedService.cs`
|
||||
|
||||
## High-Level Flow
|
||||
1) Microservice connects via messaging transport:
|
||||
- publishes a HELLO message to the gateway request queue
|
||||
2) Gateway processes HELLO:
|
||||
- registers instance + endpoints into routing state
|
||||
3) Gateway routes an HTTP request to a microservice:
|
||||
- publishes a REQUEST message to the service request queue
|
||||
4) Microservice handles request:
|
||||
- executes handler (or ASP.NET bridge) and publishes a RESPONSE message
|
||||
5) Gateway returns response to the client.
|
||||
|
||||
## Queue Topology (Conceptual)
|
||||
The Messaging transport uses a small set of queues (names are configurable):
|
||||
- **Gateway request queue**: receives HELLO / HEARTBEAT / REQUEST frames from services
|
||||
- **Gateway response queue**: receives RESPONSE frames from services
|
||||
- **Per-service request queues**: gateway publishes REQUEST frames targeted to a service
|
||||
- **Dead letter queues** (optional): for messages that exceed retries/leases
|
||||
|
||||
## Configuration (Draft)
|
||||
### Gateway
|
||||
- Register Valkey messaging services (`StellaOps.Messaging.Transport.Valkey`)
|
||||
- Add messaging transport server (`AddMessagingTransportServer`)
|
||||
- Add Gateway config section for messaging transport options:
|
||||
- Valkey connection info (host/port/auth)
|
||||
- queue naming prefix
|
||||
- consumer group / lease duration / dead-letter suffix
|
||||
|
||||
### Microservice
|
||||
- Register Valkey messaging services (`StellaOps.Messaging.Transport.Valkey`)
|
||||
- Add messaging transport client (`AddMessagingTransportClient`)
|
||||
- Ensure Microservice Router SDK connects via `IMicroserviceTransport`
|
||||
|
||||
## Operational Semantics (Draft)
|
||||
- **At-least-once** delivery: message queues and leases imply retries are possible; handlers should be idempotent where feasible.
|
||||
- **Lease timeouts**: must be tuned to max handler execution time; long-running tasks should respond with 202 + job id rather than blocking.
|
||||
- **Determinism**: message ordering may vary; Router must not depend on arrival order for correctness (only for freshness/telemetry).
|
||||
|
||||
## Security Notes
|
||||
- Messaging transport is internal. External identity must still be enforced at the Gateway.
|
||||
- The Gateway must not trust client-supplied identity headers; it must overwrite reserved headers before dispatch.
|
||||
- See `docs/modules/gateway/identity-header-policy.md`.
|
||||
|
||||
## Gaps / Implementation Work
|
||||
1) Wire Messaging transport into Gateway:
|
||||
- start/stop `MessagingTransportServer`
|
||||
- subscribe to HELLO/HEARTBEAT/RESPONSE events and reuse existing HELLO validation + routing state updates
|
||||
2) Extend Gateway transport client to support `TransportType.Messaging` for dispatch.
|
||||
3) Add config mapping and deployment examples (compose/helm) for Valkey transport.
|
||||
4) Add integration tests covering:
|
||||
- microservice HELLO registration via messaging
|
||||
- request dispatch + response return
|
||||
|
||||
120
docs/modules/router/webservice-integration-guide.md
Normal file
120
docs/modules/router/webservice-integration-guide.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Stella Router ASP.NET WebService Integration Guide
|
||||
|
||||
This guide explains how to integrate any ASP.NET Core WebService with the Stella Router for automatic endpoint discovery and dispatch.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Add a project reference to `StellaOps.Router.AspNet`:
|
||||
|
||||
```xml
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Router.AspNet/StellaOps.Router.AspNet.csproj" />
|
||||
```
|
||||
|
||||
## Integration Steps
|
||||
|
||||
### 1. Add Router Options to Service Options
|
||||
|
||||
In your service's options class (e.g., `MyServiceOptions.cs`), add:
|
||||
|
||||
```csharp
|
||||
using StellaOps.Router.AspNet;
|
||||
|
||||
public class MyServiceOptions
|
||||
{
|
||||
// ... existing options ...
|
||||
|
||||
/// <summary>
|
||||
/// Stella Router integration configuration (disabled by default).
|
||||
/// </summary>
|
||||
public StellaRouterOptionsBase? Router { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Register Services in Program.cs
|
||||
|
||||
Add the using directive:
|
||||
|
||||
```csharp
|
||||
using StellaOps.Router.AspNet;
|
||||
```
|
||||
|
||||
After service registration (e.g., after `AddControllers()`), add:
|
||||
|
||||
```csharp
|
||||
// Stella Router integration
|
||||
builder.Services.TryAddStellaRouter(
|
||||
serviceName: "my-service-name",
|
||||
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
|
||||
routerOptions: options.Router);
|
||||
```
|
||||
|
||||
### 3. Enable Middleware
|
||||
|
||||
After `UseAuthorization()`, add:
|
||||
|
||||
```csharp
|
||||
app.TryUseStellaRouter(resolvedOptions.Router);
|
||||
```
|
||||
|
||||
### 4. Refresh Endpoint Cache
|
||||
|
||||
After all endpoints are mapped (before `app.RunAsync()`), add:
|
||||
|
||||
```csharp
|
||||
app.TryRefreshStellaRouterEndpoints(resolvedOptions.Router);
|
||||
```
|
||||
|
||||
## Configuration Example (YAML)
|
||||
|
||||
```yaml
|
||||
myservice:
|
||||
router:
|
||||
enabled: true
|
||||
region: "us-east-1"
|
||||
defaultTimeoutSeconds: 30
|
||||
heartbeatIntervalSeconds: 10
|
||||
gateways:
|
||||
- host: "router.stellaops.local"
|
||||
port: 9100
|
||||
transportType: "Tcp"
|
||||
useTls: true
|
||||
certificatePath: "/etc/certs/router.pem"
|
||||
```
|
||||
|
||||
## WebServices Requiring Integration
|
||||
|
||||
The following WebServices need to be updated with Router integration:
|
||||
|
||||
| Service | Path | Status |
|
||||
|---------|------|--------|
|
||||
| Scanner.WebService | `src/Scanner/StellaOps.Scanner.WebService` | ✅ Complete |
|
||||
| Concelier.WebService | `src/Concelier/StellaOps.Concelier.WebService` | Pending |
|
||||
| Excititor.WebService | `src/Excititor/StellaOps.Excititor.WebService` | Pending |
|
||||
| Gateway.WebService | `src/Gateway/StellaOps.Gateway.WebService` | Pending |
|
||||
| VexHub.WebService | `src/VexHub/StellaOps.VexHub.WebService` | Pending |
|
||||
| Attestor.WebService | `src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService` | Pending |
|
||||
| EvidenceLocker.WebService | `src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.WebService` | Pending |
|
||||
| Findings.Ledger.WebService | `src/Findings/StellaOps.Findings.Ledger.WebService` | Pending |
|
||||
| AdvisoryAI.WebService | `src/AdvisoryAI/StellaOps.AdvisoryAI.WebService` | Pending |
|
||||
| IssuerDirectory.WebService | `src/IssuerDirectory/StellaOps.IssuerDirectory/StellaOps.IssuerDirectory.WebService` | Pending |
|
||||
| Notifier.WebService | `src/Notifier/StellaOps.Notifier/StellaOps.Notifier.WebService` | Pending |
|
||||
| Notify.WebService | `src/Notify/StellaOps.Notify.WebService` | Pending |
|
||||
| PacksRegistry.WebService | `src/PacksRegistry/StellaOps.PacksRegistry/StellaOps.PacksRegistry.WebService` | Pending |
|
||||
| RiskEngine.WebService | `src/RiskEngine/StellaOps.RiskEngine/StellaOps.RiskEngine.WebService` | Pending |
|
||||
| Signer.WebService | `src/Signer/StellaOps.Signer/StellaOps.Signer.WebService` | Pending |
|
||||
| TaskRunner.WebService | `src/TaskRunner/StellaOps.TaskRunner/StellaOps.TaskRunner.WebService` | Pending |
|
||||
| TimelineIndexer.WebService | `src/TimelineIndexer/StellaOps.TimelineIndexer/StellaOps.TimelineIndexer.WebService` | Pending |
|
||||
| Orchestrator.WebService | `src/Orchestrator/StellaOps.Orchestrator/StellaOps.Orchestrator.WebService` | Pending |
|
||||
| Scheduler.WebService | `src/Scheduler/StellaOps.Scheduler.WebService` | Pending |
|
||||
| ExportCenter.WebService | `src/ExportCenter/StellaOps.ExportCenter/StellaOps.ExportCenter.WebService` | Pending |
|
||||
|
||||
## Files Created
|
||||
|
||||
The Router.AspNet library includes the following files:
|
||||
|
||||
- `StellaOps.Router.AspNet.csproj` - Project file
|
||||
- `StellaRouterOptions.cs` - Unified router options
|
||||
- `StellaRouterExtensions.cs` - DI extensions (`AddStellaRouter`, `UseStellaRouter`)
|
||||
- `CompositeRequestDispatcher.cs` - Routes requests to ASP.NET or Stella endpoints
|
||||
- `StellaRouterOptionsBase.cs` - Base options class for embedding in service options
|
||||
- `StellaRouterIntegrationHelper.cs` - Helper methods for conditional integration
|
||||
Reference in New Issue
Block a user