# 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? 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 RequiringClaims { get; init; } = []; public IReadOnlyList AuthorizationPolicies { get; init; } = []; public IReadOnlyList Roles { get; init; } = []; public bool AllowAnonymous { get; init; } public AuthorizationSource AuthorizationSource { get; init; } // === Parameters === public IReadOnlyList Parameters { get; init; } = []; // === Responses === public IReadOnlyList Responses { get; init; } = []; // === OpenAPI === public string? OperationId { get; init; } public string? Summary { get; init; } public string? Description { get; init; } public IReadOnlyList Tags { get; init; } = []; // === Schema === public EndpointSchemaInfo? SchemaInfo { get; init; } // === Internal === internal RouteEndpoint? OriginalEndpoint { get; init; } internal string? OriginalRoutePattern { get; init; } /// /// Convert to standard EndpointDescriptor for HELLO payload. /// 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 { /// /// Discover ASP.NET endpoints with full metadata. /// IReadOnlyList DiscoverAspNetEndpoints(); } ``` ### 4. IAuthorizationClaimMapper Authorization-to-claims mapping interface: ```csharp public interface IAuthorizationClaimMapper { /// /// Map ASP.NET authorization metadata to Router claim requirements. /// Task MapAsync( RouteEndpoint endpoint, CancellationToken cancellationToken = default); } public sealed record AuthorizationMappingResult { public IReadOnlyList Claims { get; init; } = []; public IReadOnlyList Policies { get; init; } = []; public IReadOnlyList Roles { get; init; } = []; public bool AllowAnonymous { get; init; } public AuthorizationSource Source { get; init; } } ``` ### 5. IAspNetRouterRequestDispatcher Request dispatch interface: ```csharp public interface IAspNetRouterRequestDispatcher { /// /// Dispatch a Router request frame through ASP.NET pipeline. /// Task DispatchAsync( RequestFrame request, CancellationToken cancellationToken = default); } ``` --- ## Endpoint Discovery Algorithm ### Step 1: Enumerate Endpoints ```csharp var endpoints = endpointDataSource.Endpoints .OfType() .Where(e => e.Metadata.GetMetadata() 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> ResolvePolicyAsync( string policyName, IAuthorizationPolicyProvider policyProvider) { var policy = await policyProvider.GetPolicyAsync(policyName); if (policy is null) return []; var claims = new List(); 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 MergeWithYaml( IReadOnlyList 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 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 headers) { var claims = new List(); 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 configure) { var options = new StellaRouterBridgeOptions { ServiceName = "", Version = "", Region = "" }; configure(options); ValidateOptions(options); services.AddSingleton(options); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // Register as IEndpointDiscoveryProvider for Router SDK integration services.AddSingleton(sp => sp.GetRequiredService()); // 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() ?? 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()` | ✓ | 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`