RFC 6570 Variable Mapping

Document OpenAPI 3.2 RFC 6570 path expressions and map ASP.NET Core route values into RFC6570 variables for multi-segment paths.

Full source

File: pwsh/tutorial/examples/10.23-OpenAPI-Path-Template-Mapping.ps1

<#
    Sample: OpenAPI 3.2 RFC 6570 Path Template Mapping
    Purpose: Demonstrate RFC 6570 URI Template mapping from ASP.NET Core route values
    File:    10.23-OpenAPI-Path-Template-Mapping.ps1
    Notes:   - Shows how route parameters are extracted and mapped to RFC 6570 variables
             - Demonstrates simple parameters, reserved operators, and explode (multi-segment)
             - Demonstrates how mapped variables can drive callback/link template expansion
#>

param(
    [int]$Port = 5000,
    [IPAddress]$IPAddress = [IPAddress]::Loopback
)

if (-not (Get-Module Kestrun)) { Import-Module Kestrun }

# --- Logging / Server ---
New-KrLogger | Add-KrSinkConsole |
    Set-KrLoggerLevel -Value Debug |
    Register-KrLogger -Name 'console' -SetAsDefault

New-KrServer -Name 'RFC 6570 Variable Mapping'
Add-KrEndpoint -Port $Port -IPAddress $IPAddress

# =========================================================
#                 TOP-LEVEL OPENAPI
# =========================================================

Add-KrOpenApiInfo -Title 'RFC 6570 Variable Mapping API' `
    -Version '1.0.0' `
    -Description 'Demonstrates RFC 6570 URI Template variable mapping for OpenAPI 3.2 path expressions.'

Add-KrOpenApiTag -Name 'Users' -Description 'User management endpoints'
Add-KrOpenApiTag -Name 'Files' -Description 'File access endpoints'
Add-KrOpenApiTag -Name 'API' -Description 'Versioned API endpoints'

# =========================================================
#                     COMPONENT SCHEMAS
# =========================================================

[OpenApiSchemaComponent(Description = 'User information', RequiredProperties = ('id', 'username'))]
class User {
    [OpenApiProperty(Description = 'User ID', Example = 42)]
    [int]$id

    [OpenApiProperty(Description = 'Username', Example = 'johndoe')]
    [string]$username

    [OpenApiProperty(Description = 'Email address', Format = 'email', Example = 'john@example.com')]
    [string]$email
}

[OpenApiSchemaComponent(Description = 'File metadata')]
class FileInfo {
    [OpenApiProperty(Description = 'File path', Example = 'documents/report.pdf')]
    [string]$path

    [OpenApiProperty(Description = 'File size in bytes', Example = 1024)]
    [long]$size

    [OpenApiProperty(Description = 'Content type', Example = 'application/pdf')]
    [string]$contentType
}

[OpenApiSchemaComponent(Description = 'Variable mapping result')]
class VariableMapping {
    [OpenApiProperty(Description = 'RFC 6570 template used', Example = '/api/v{version}/users/{userId}')]
    [string]$template

    [OpenApiProperty(Description = 'Extracted variables', Example = '{"version":"1","userId":"42"}')]
    [hashtable]$variables
}

# =========================================================
#                 ROUTES / OPERATIONS
# =========================================================


# =========================================================
#       SIMPLE PARAMETER EXTRACTION
# =========================================================

<#
.SYNOPSIS
    Get user by ID.
.DESCRIPTION
    Returns a user with the specified ID.
.PARAMETER userId
    User identifier.
.NOTES
    GET endpoint: Return a user by ID
#>
function getUser {
    [OpenApiPath(HttpVerb = 'get', Pattern = '/users/{userId}', Tags = 'Users')]
    [OpenApiResponse(StatusCode = '200', Description = 'User found', Schema = [User])]
    [OpenApiResponse(StatusCode = '404', Description = 'User not found')]
    param(
        [openApiParameter(In = 'path', Required = $true)]
        [string]$userId
    )

    $userId = Get-KrRequestRouteParam -Name 'userId'

    Write-KrJsonResponse @{
        id = [int]$userId
        username = "user$userId"
        email = "user${userId}@example.com"
    } -StatusCode 200
}

<#
.SYNOPSIS
    Get user by ID with API version.
.DESCRIPTION
    Returns a user with the specified API version extracted from the path.
.PARAMETER version
    API version.
.PARAMETER userId
    User identifier.
.NOTES
    GET endpoint: Return a user by ID with versioning
#>
function getApiUser {
    [OpenApiPath(HttpVerb = 'get', Pattern = '/api/v{version}/users/{userId}', Tags = 'API')]
    [OpenApiResponse(StatusCode = '200', Description = 'User found with version info', Schema = [User])]
    param(
        [openApiParameter(In = 'path', Required = $true)]
        [string]$version,

        [openApiParameter(In = 'path', Required = $true)]
        [string]$userId
    )

    Write-KrJsonResponse @{
        id = [int]$userId
        username = "user$userId"
        email = "user${userId}@example.com"
        apiVersion = "v$version"
    } -StatusCode 200
}

# =========================================================
#       RESERVED OPERATOR / EXPLODE (MULTI-SEGMENT)
# =========================================================

function getFile {
    [OpenApiPath(HttpVerb = 'get', Pattern = '/files/{+path}', Tags = 'Files')]
    [OpenApiResponse(StatusCode = '200', Description = 'File metadata', Schema = [FileInfo])]
    [OpenApiResponse(StatusCode = '404', Description = 'File not found')]
    param(
        [openApiParameter(In = 'path', Description = 'File path', Required = $true)]
        [string]$path
    )

    #  $filePath = Get-KrRequestRouteParam -Name 'path'

    Write-KrJsonResponse @{
        path = $path
        size = 1024
        contentType = 'application/octet-stream'
    } -StatusCode 200
}

# =========================================================
#       VARIABLE MAPPING INTROSPECTION
# =========================================================

<#
.SYNOPSIS
    Get variable mapping details.
.DESCRIPTION
    Returns details about variable mapping for a given template and ID.
.PARAMETER template
    Template name.
.PARAMETER id
    Identifier.
.NOTES
    GET endpoint: Return variable mapping details
#>
function getVariableMapping {
    [OpenApiPath(HttpVerb = 'get', Pattern = '/mapping/{template}/{id}', Tags = 'API')]
    [OpenApiResponse(StatusCode = '200', Description = 'Variable mapping details', Schema = [VariableMapping])]
    param(
        [openApiParameter(In = 'path', Required = $true)]
        [string]$template,

        [openApiParameter(In = 'path', Required = $true)]
        [string]$id
    )

    $template = Get-KrRequestRouteParam -Name 'template'
    $id = Get-KrRequestRouteParam -Name 'id'

    # Demonstrate variable extraction using the current OpenAPI path expression
    $routePattern = '/mapping/{template}/{id}'

    Write-KrJsonResponse @{
        template = $routePattern
        variables = @{
            template = $template
            id = $id
        }
        extractedFrom = 'ASP.NET Core RouteValues'
        rfc6570Compliant = $true
    } -StatusCode 200
}

# =========================================================
#                     BUILD & START
# =========================================================
Enable-KrConfiguration

Add-KrOpenApiRoute -SpecVersion OpenApi3_2
Build-KrOpenApiDocument

Add-KrApiDocumentationRoute -DocumentType Redoc -OpenApiEndpoint '/openapi/v3.2/openapi.yaml'
Add-KrApiDocumentationRoute -DocumentType Elements -OpenApiEndpoint '/openapi/v3.2/openapi.yaml'
Add-KrApiDocumentationRoute -DocumentType Scalar -OpenApiEndpoint '/openapi/v3.2/openapi.yaml'

Start-KrServer

Step-by-step

  1. Logging: Register the console logger as default.
  2. Server: Create a server and listener (IP + port).
  3. OpenAPI Info + Tags: Add document metadata and tags (Users, Files, API).
  4. Schema Components: Define User, FileInfo, and VariableMapping as reusable schema components.
  5. Simple routes: Define OpenAPI-annotated routes like /users/{userId} and /api/v{version}/users/{userId}.
  6. Multi-segment paths: Document the route as /files/{+path} (RFC6570 reserved expansion). Kestrun maps this to the Kestrel catch-all route /files/{**path} automatically.
  7. Introspection route: Return a small JSON payload from /mapping/{template}/{id} to demonstrate captured route values.
  8. Build + Run: Enable configuration, expose OpenAPI routes, build the OpenAPI document, add Swagger/ReDoc routes, and start the server.

Try it

Start the server:

pwsh .\docs\_includes\examples\pwsh\10.23-OpenAPI-Path-Template-Mapping.ps1

Call the routes:

curl -i http://127.0.0.1:5000/users/42
curl -i http://127.0.0.1:5000/api/v1/users/123
curl -i http://127.0.0.1:5000/files/documents/reports/2024/annual.pdf
curl -i http://127.0.0.1:5000/mapping/mytemplate/999

Fetch the OpenAPI 3.2 document and open Swagger:

curl http://127.0.0.1:5000/openapi/v3.2/openapi.json
Start-Process http://127.0.0.1:5000/docs/swagger

Key points

  • OpenAPI 3.2 uses RFC6570 URI templates for path expressions (e.g. {+path} for multi-segment path variables).
  • Kestrun maps RFC6570 multi-segment variables to Kestrel catch-all parameters ({**path}) at runtime.
  • Kestrun bridges the gap by mapping ASP.NET route values into RFC6570 variable assignments.

Troubleshooting

  • /files/... returns 404: Ensure the OpenAPI template uses RFC6570 multi-segment syntax (e.g. /files/{+path} or /files/{path*}).
  • OpenAPI does not show {+path}: Ensure the OpenAPI template is documented with RFC6570 syntax (e.g. Pattern = '/files/{+path}').
  • Startup errors: Ensure Enable-KrConfiguration runs after defining routes and OpenAPI metadata, and before Start-KrServer.

References


Previous / Next

Previous: XML Modeling Next: Razor