< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Hosting.KestrunHostMapExtensions
Assembly: Kestrun
File(s): File 1: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Hosting/KestrunHostMapExtensions_FormRoutes.cs
File 2: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Hosting/KestrunHostMapExtensions.cs
Tag: Kestrun/Kestrun@5f1d2b981c9d7292c11fd448428c6ab6c811c5de
Line coverage
83%
Covered lines: 638
Uncovered lines: 128
Coverable lines: 766
Total lines: 1983
Line coverage: 83.2%
Branch coverage
78%
Covered branches: 306
Total branches: 391
Branch coverage: 78.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 11/19/2025 - 17:40:50 Line coverage: 71.1% (286/402) Branch coverage: 81.3% (170/209) Total lines: 1146 Tag: Kestrun/Kestrun@fcf33342333cef0516fe0d0912a86709874fd02612/12/2025 - 17:27:19 Line coverage: 34.5% (185/535) Branch coverage: 49.7% (114/229) Total lines: 1429 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/14/2025 - 20:04:52 Line coverage: 34.4% (186/540) Branch coverage: 49.3% (115/233) Total lines: 1439 Tag: Kestrun/Kestrun@a05ac8de57c6207e227b92ba360e9d58869ac80a12/15/2025 - 02:23:46 Line coverage: 52.5% (284/540) Branch coverage: 73.8% (172/233) Total lines: 1439 Tag: Kestrun/Kestrun@7a3839f4de2254e22daae81ab8dc7cb2f40c833012/18/2025 - 21:41:58 Line coverage: 52.5% (284/540) Branch coverage: 73.8% (172/233) Total lines: 1440 Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff01/02/2026 - 00:16:25 Line coverage: 52.7% (284/538) Branch coverage: 73.8% (172/233) Total lines: 1438 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/17/2026 - 04:33:35 Line coverage: 52.5% (284/540) Branch coverage: 72.5% (172/237) Total lines: 1442 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba01/23/2026 - 00:12:18 Line coverage: 53.1% (291/547) Branch coverage: 72.3% (173/239) Total lines: 1460 Tag: Kestrun/Kestrun@67ed8a99376189d7ed94adba1b1854518edd75d902/05/2026 - 00:28:18 Line coverage: 38.2% (291/760) Branch coverage: 44.9% (173/385) Total lines: 1971 Tag: Kestrun/Kestrun@d9261bd752e45afa789d10bc0c82b7d5724d958902/18/2026 - 08:33:07 Line coverage: 38.2% (291/760) Branch coverage: 44.7% (173/387) Total lines: 1971 Tag: Kestrun/Kestrun@bf8a937cfb7e8936c225b9df4608f8ddd85558b103/26/2026 - 03:54:59 Line coverage: 82.5% (627/760) Branch coverage: 77.7% (301/387) Total lines: 1971 Tag: Kestrun/Kestrun@844b5179fb0492dc6b1182bae3ff65fa7365521d04/23/2026 - 14:35:41 Line coverage: 83.2% (638/766) Branch coverage: 78.2% (306/391) Total lines: 1983 Tag: Kestrun/Kestrun@2fdbb120ca2faaa9acf2b8d2a34a7d64b067edbe 11/19/2025 - 17:40:50 Line coverage: 71.1% (286/402) Branch coverage: 81.3% (170/209) Total lines: 1146 Tag: Kestrun/Kestrun@fcf33342333cef0516fe0d0912a86709874fd02612/12/2025 - 17:27:19 Line coverage: 34.5% (185/535) Branch coverage: 49.7% (114/229) Total lines: 1429 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/14/2025 - 20:04:52 Line coverage: 34.4% (186/540) Branch coverage: 49.3% (115/233) Total lines: 1439 Tag: Kestrun/Kestrun@a05ac8de57c6207e227b92ba360e9d58869ac80a12/15/2025 - 02:23:46 Line coverage: 52.5% (284/540) Branch coverage: 73.8% (172/233) Total lines: 1439 Tag: Kestrun/Kestrun@7a3839f4de2254e22daae81ab8dc7cb2f40c833012/18/2025 - 21:41:58 Line coverage: 52.5% (284/540) Branch coverage: 73.8% (172/233) Total lines: 1440 Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff01/02/2026 - 00:16:25 Line coverage: 52.7% (284/538) Branch coverage: 73.8% (172/233) Total lines: 1438 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/17/2026 - 04:33:35 Line coverage: 52.5% (284/540) Branch coverage: 72.5% (172/237) Total lines: 1442 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba01/23/2026 - 00:12:18 Line coverage: 53.1% (291/547) Branch coverage: 72.3% (173/239) Total lines: 1460 Tag: Kestrun/Kestrun@67ed8a99376189d7ed94adba1b1854518edd75d902/05/2026 - 00:28:18 Line coverage: 38.2% (291/760) Branch coverage: 44.9% (173/385) Total lines: 1971 Tag: Kestrun/Kestrun@d9261bd752e45afa789d10bc0c82b7d5724d958902/18/2026 - 08:33:07 Line coverage: 38.2% (291/760) Branch coverage: 44.7% (173/387) Total lines: 1971 Tag: Kestrun/Kestrun@bf8a937cfb7e8936c225b9df4608f8ddd85558b103/26/2026 - 03:54:59 Line coverage: 82.5% (627/760) Branch coverage: 77.7% (301/387) Total lines: 1971 Tag: Kestrun/Kestrun@844b5179fb0492dc6b1182bae3ff65fa7365521d04/23/2026 - 14:35:41 Line coverage: 83.2% (638/766) Branch coverage: 78.2% (306/391) Total lines: 1983 Tag: Kestrun/Kestrun@2fdbb120ca2faaa9acf2b8d2a34a7d64b067edbe

Coverage delta

Coverage delta 45 -45

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: AddFormRoute(...)87.5%88100%
File 1: CreateLanguageOptions(...)100%11100%
File 1: BuildFormRouteMapOptions(...)100%22100%
File 1: ApplyAuthorizationOptions(...)86.36%2222100%
File 1: ApplyOpenApiMetadata(...)71.43%1414100%
File 1: GetFormRouteWrapperScript(...)100%11100%
File 1: BuildOpenApiRequestBody(...)91.67%2424100%
File 1: IsProbablyFileRule(...)100%1414100%
File 1: CreateRuleSchema(...)100%44100%
File 1: MergeMapRouteOptions(...)54.55%2222100%
File 1: MergePattern(...)50%5460%
File 1: MergeUnique(...)100%66100%
File 1: AddNonEmptyValues(...)83.33%6683.33%
File 1: MergeRefs(...)25%22840%
File 1: MergeArguments(...)75%121287.5%
File 2: TryParse(...)100%11100%
File 2: AddMapRoute(...)100%11100%
File 2: AddMapRoute(...)100%22100%
File 2: AddMapRoute(...)80%101090.32%
File 2: AddOpenApiMapRoute(...)100%1191.84%
File 2: AddMapRoute(...)83.33%66100%
File 2: AddMapRoute(...)100%44100%
File 2: AddMapRoute(...)100%44100%
File 2: CreateMapRoute(...)75%5461.11%
File 2: ValidateRouteOptions(...)91.67%121293.75%
File 2: CompileScript(...)57.14%8770%
File 2: handler()50%5460%
File 2: CreateAndRegisterRoute(...)83.33%6691.67%
File 2: AddMapOptions(...)100%11100%
File 2: TryParseEndpointSpec(...)100%88100%
File 2: TryParseUrlSpec(...)81.25%161694.44%
File 2: TryParseBracketedIpv6Spec(...)66.67%6687.5%
File 2: TryParseHostPortSpec(...)100%66100%
File 2: IsValidPort(...)100%22100%
File 2: ToRequireHost(...)100%22100%
File 2: IsIPv6Address(...)100%22100%
File 2: ApplyRequiredHost(...)96.15%2626100%
File 2: ApplyKestrunConventions(...)100%11100%
File 2: AddMetadata(...)41.67%731225%
File 2: ApplyShortCircuit(...)25%10428.57%
File 2: ApplyAnonymous(...)100%22100%
File 2: DisableAntiforgery(...)100%22100%
File 2: DisableResponseCompression(...)100%22100%
File 2: ApplyRateLimiting(...)50%3240%
File 2: ApplyAuthSchemes(...)100%88100%
File 2: ApplyPolicies(...)100%88100%
File 2: ApplyCors(...)33.33%14640%
File 2: ApplyOpenApiMetadata(...)0%7280%
File 2: AddHtmlTemplateRoute(...)0%620%
File 2: AddHtmlTemplateRoute(...)88.89%221876.92%
File 2: AddSwaggerUiRoute(...)100%11100%
File 2: AddRedocUiRoute(...)100%210%
File 2: AddScalarUiRoute(...)100%210%
File 2: AddRapiDocUiRoute(...)100%210%
File 2: AddElementsUiRoute(...)100%210%
File 2: AddOpenApiUiRoute(...)78.57%261460.71%
File 2: AddHtmlRouteFromEmbeddedResource(...)100%11100%
File 2: MapExists(...)100%11100%
File 2: MapExists(...)100%11100%
File 2: GetMapRouteOptions(...)50%22100%
File 2: NormalizeCatchAllPattern(...)50%22100%
File 2: AddAntiforgeryTokenRoute(...)50%2297.3%
File 2: IsUnsafeVerb(...)80%1010100%
File 2: IsUnsafeMethod(...)100%66100%
File 2: ShouldValidateCsrf(...)100%66100%
File 2: TryValidateAntiforgeryAsync()50%4223.53%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Hosting/KestrunHostMapExtensions_FormRoutes.cs

#LineLine coverage
 1using System.Management.Automation;
 2using Kestrun.Forms;
 3using Kestrun.Hosting.Options;
 4using Kestrun.OpenApi;
 5using Kestrun.Scripting;
 6using Kestrun.Utilities;
 7using Microsoft.OpenApi;
 8
 9namespace Kestrun.Hosting;
 10
 11public static partial class KestrunHostMapExtensions
 12{
 13    /// <summary>
 14    /// Adds a POST route that parses form payloads using <see cref="KrFormParser"/>, injects the parsed payload into th
 15    /// runspace as <c>$FormPayload</c>, and then
 16    /// executes the provided PowerShell <paramref name="userScriptBlock"/>.
 17    ///
 18    /// By default, only <c>multipart/form-data</c> is accepted; additional request content types (such as
 19    /// <c>application/x-www-form-urlencoded</c> and <c>multipart/mixed</c>) are opt-in via
 20    /// <see cref="KrFormOptions.AllowedContentTypes"/>.
 21    ///
 22    /// This method also fills <see cref="MapRouteOptions.OpenAPI"/> (unless disabled) so the route appears in generated
 23    /// OpenAPI documents.
 24    /// </summary>
 25    /// <param name="host">The Kestrun host.</param>
 26    /// <param name="pattern">The route pattern (e.g. <c>/upload</c>).</param>
 27    /// <param name="userScriptBlock">The PowerShell scriptblock to execute after parsing.</param>
 28    /// <param name="formOptions">Form parsing options (null uses defaults).</param>
 29    /// <param name="authorizationSchemes">Authorization schemes (optional).</param>
 30    /// <param name="authorizationPolicies">Authorization policies (optional).</param>
 31    /// <param name="corsPolicy">CORS policy name (optional).</param>
 32    /// <param name="allowAnonymous">Whether to allow anonymous access.</param>
 33    /// <returns>The host for chaining.</returns>
 34    public static KestrunHost AddFormRoute(
 35        this KestrunHost host,
 36        string pattern,
 37        ScriptBlock userScriptBlock,
 38        KrFormOptions? formOptions,
 39        string[]? authorizationSchemes,
 40        string[]? authorizationPolicies,
 41        string? corsPolicy,
 42        bool allowAnonymous)
 43    {
 144        ArgumentNullException.ThrowIfNull(host);
 145        ArgumentException.ThrowIfNullOrWhiteSpace(pattern);
 146        ArgumentNullException.ThrowIfNull(userScriptBlock);
 47
 148        formOptions ??= new KrFormOptions();
 149        formOptions.Logger ??= host.Logger;
 50
 151        var mapOptions = BuildFormRouteMapOptions(
 152            pattern,
 153            userScriptBlock,
 154            formOptions,
 155            authorizationSchemes,
 156            authorizationPolicies,
 157            corsPolicy,
 158            allowAnonymous);
 59
 160        if (host.RouteGroupStack.Count > 0 && host.RouteGroupStack.Peek() is MapRouteOptions parent)
 61        {
 162            mapOptions = MergeMapRouteOptions(parent, mapOptions);
 63        }
 64
 165        return host.AddMapRoute(mapOptions);
 66    }
 67
 68    /// <summary>
 69    /// Creates language options for the form route script.
 70    /// </summary>
 71    /// <param name="formOptions">Form parsing options.</param>
 72    /// <param name="userScriptBlock">The PowerShell scriptblock to execute after parsing.</param>
 73    /// <returns>Language options for the script.</returns>
 74    private static LanguageOptions CreateLanguageOptions(KrFormOptions formOptions, ScriptBlock userScriptBlock)
 75    {
 376        return new LanguageOptions
 377        {
 378            Language = ScriptLanguage.PowerShell,
 379            Code = GetFormRouteWrapperScript(userScriptBlock),
 380            Arguments = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
 381            {
 382                ["__KrOptions"] = formOptions
 383            }
 384        };
 85    }
 86
 87    /// <summary>
 88    /// Builds map route options for a form route.
 89    /// </summary>
 90    /// <param name="pattern">The route pattern (e.g. <c>/upload</c>).</param>
 91    /// <param name="userScriptBlock">The PowerShell scriptblock to execute after parsing.</param>
 92    /// <param name="formOptions">Form parsing options.</param>
 93    /// <param name="authorizationSchemes">Authorization schemes (optional).</param>
 94    /// <param name="authorizationPolicies">Authorization policies (optional).</param>
 95    /// <param name="corsPolicy">CORS policy name (optional).</param>
 96    /// <param name="allowAnonymous">Whether to allow anonymous access.</param>
 97    /// <param name="disableOpenApi">Whether to disable OpenAPI metadata generation.</param>
 98    /// <param name="openApiOperationId">OpenAPI operation ID (optional).</param>
 99    /// <param name="openApiTags">OpenAPI tags (optional).</param>
 100    /// <param name="openApiSummary">OpenAPI summary (optional).</param>
 101    /// <param name="openApiDescription">OpenAPI description (optional).</param>
 102    /// <param name="openApiDocumentId">OpenAPI document ID(s) (optional).</param>
 103    /// <returns>Map route options configured for the form route.</returns>
 104    /// <exception cref="ArgumentException">Thrown when allowAnonymous is true and authorizationSchemes or authorization
 105    private static MapRouteOptions BuildFormRouteMapOptions(
 106        string pattern,
 107        ScriptBlock userScriptBlock,
 108        KrFormOptions formOptions,
 109        string[]? authorizationSchemes,
 110        string[]? authorizationPolicies,
 111        string? corsPolicy,
 112        bool allowAnonymous,
 113        bool disableOpenApi = true,
 114        string? openApiOperationId = null,
 115        string[]? openApiTags = null,
 116        string? openApiSummary = null,
 117        string? openApiDescription = null,
 118        string[]? openApiDocumentId = null
 119      )
 120    {
 3121        var routeOptions = new MapRouteOptions
 3122        {
 3123            Pattern = pattern,
 3124            HttpVerbs = [HttpVerb.Post],
 3125            AllowAnonymous = allowAnonymous,
 3126            CorsPolicy = corsPolicy ?? string.Empty,
 3127            ScriptCode = CreateLanguageOptions(formOptions, userScriptBlock),
 3128            FormOptions = formOptions
 3129        };
 130
 3131        ApplyAuthorizationOptions(routeOptions, allowAnonymous, authorizationSchemes, authorizationPolicies);
 2132        ApplyOpenApiMetadata(
 2133            routeOptions,
 2134            pattern,
 2135            formOptions,
 2136            disableOpenApi,
 2137            openApiOperationId,
 2138            openApiTags,
 2139            openApiSummary,
 2140            openApiDescription,
 2141            openApiDocumentId);
 142
 2143        return routeOptions;
 144    }
 145
 146    /// <summary>
 147    /// Applies authorization configuration to the route options.
 148    /// </summary>
 149    /// <param name="routeOptions">The route options to update.</param>
 150    /// <param name="allowAnonymous">Whether to allow anonymous access.</param>
 151    /// <param name="authorizationSchemes">Authorization schemes (optional).</param>
 152    /// <param name="authorizationPolicies">Authorization policies (optional).</param>
 153    /// <exception cref="ArgumentException">
 154    /// Thrown when <paramref name="allowAnonymous"/> is true and authorization schemes or policies are specified.
 155    /// </exception>
 156    private static void ApplyAuthorizationOptions(
 157        MapRouteOptions routeOptions,
 158        bool allowAnonymous,
 159        string[]? authorizationSchemes,
 160        string[]? authorizationPolicies)
 161    {
 3162        if (allowAnonymous)
 163        {
 2164            if (authorizationSchemes is { Length: > 0 } || authorizationPolicies is { Length: > 0 })
 165            {
 1166                throw new ArgumentException(
 1167                    "The allowAnonymous flag cannot be used together with authorizationSchemes or authorizationPolicies.
 168            }
 169
 1170            return;
 171        }
 172
 1173        if (authorizationSchemes is { Length: > 0 })
 174        {
 1175            routeOptions.RequireSchemes.AddRange(authorizationSchemes ?? []);
 176        }
 177
 1178        if (authorizationPolicies is { Length: > 0 })
 179        {
 1180            routeOptions.RequirePolicies.AddRange(authorizationPolicies ?? []);
 181        }
 1182    }
 183
 184    /// <summary>
 185    /// Applies OpenAPI metadata to the route options when enabled.
 186    /// </summary>
 187    /// <param name="routeOptions">The route options to update.</param>
 188    /// <param name="pattern">The route pattern.</param>
 189    /// <param name="formOptions">Form parsing options.</param>
 190    /// <param name="disableOpenApi">Whether to disable OpenAPI metadata generation.</param>
 191    /// <param name="openApiOperationId">OpenAPI operation ID (optional).</param>
 192    /// <param name="openApiTags">OpenAPI tags (optional).</param>
 193    /// <param name="openApiSummary">OpenAPI summary (optional).</param>
 194    /// <param name="openApiDescription">OpenAPI description (optional).</param>
 195    /// <param name="openApiDocumentId">OpenAPI document ID(s) (optional).</param>
 196    private static void ApplyOpenApiMetadata(
 197        MapRouteOptions routeOptions,
 198        string pattern,
 199        KrFormOptions formOptions,
 200        bool disableOpenApi,
 201        string? openApiOperationId,
 202        string[]? openApiTags,
 203        string? openApiSummary,
 204        string? openApiDescription,
 205        string[]? openApiDocumentId)
 206    {
 2207        if (disableOpenApi)
 208        {
 1209            return;
 210        }
 211
 1212        var meta = new OpenAPIPathMetadata(pattern, routeOptions)
 1213        {
 1214            PathLikeKind = OpenApiPathLikeKind.Path,
 1215            OperationId = string.IsNullOrWhiteSpace(openApiOperationId) ? null : openApiOperationId,
 1216            Summary = string.IsNullOrWhiteSpace(openApiSummary) ? null : openApiSummary,
 1217            Description = string.IsNullOrWhiteSpace(openApiDescription) ? null : openApiDescription,
 1218            DocumentId = openApiDocumentId
 1219        };
 220
 1221        if (openApiTags is { Length: > 0 })
 222        {
 1223            meta.Tags.AddRange(openApiTags ?? []);
 224        }
 225
 1226        meta.RequestBody = BuildOpenApiRequestBody(formOptions);
 227
 228        // Let the descriptor generate a default 200 response.
 1229        meta.Responses = null;
 230
 1231        routeOptions.OpenAPI[HttpVerb.Post] = meta;
 1232    }
 233
 234    private static string GetFormRouteWrapperScript(ScriptBlock scriptBlock)
 235    {
 236        // NOTE: We recreate the ScriptBlock inside the request runspace so it executes with the request's
 237        // session state (including $Context and Kestrun cmdlets).
 3238        return @"
 3239##############################
 3240# Form Route Wrapper
 3241##############################
 3242$FormPayload = $null
 3243try {
 3244    $FormPayload = [Kestrun.Forms.KrFormParser]::Parse($Context.HttpContext, $__KrOptions, $Context.Ct)
 3245} catch [Kestrun.Forms.KrFormException] {
 3246    $ex = $_.Exception
 3247    Write-KrTextResponse -InputObject $ex.Message -StatusCode $ex.StatusCode
 3248    return
 3249}
 3250
 3251############################
 3252# User Scriptblock
 3253############################
 3254
 3255" + scriptBlock.ToString();
 256    }
 257
 258    internal static OpenApiRequestBody BuildOpenApiRequestBody(KrFormOptions options)
 259    {
 2260        var required = new HashSet<string>(StringComparer.Ordinal);
 261
 2262        var multipartProps = new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
 2263        var urlEncodedProps = new Dictionary<string, IOpenApiSchema>(StringComparer.Ordinal);
 2264        var multipartEncoding = new Dictionary<string, OpenApiEncoding>(StringComparer.Ordinal);
 265
 12266        foreach (var rule in options.Rules)
 267        {
 4268            if (string.IsNullOrWhiteSpace(rule.Name))
 269            {
 270                continue;
 271            }
 272
 4273            if (rule.Required)
 274            {
 2275                _ = required.Add(rule.Name);
 276            }
 277
 4278            var isFile = IsProbablyFileRule(rule);
 4279            var multipartSchema = CreateRuleSchema(isFile, rule.AllowMultiple);
 4280            multipartProps[rule.Name] = multipartSchema;
 281
 4282            if (isFile && rule.AllowedContentTypes.Count > 0)
 283            {
 1284                multipartEncoding[rule.Name] = new OpenApiEncoding
 1285                {
 1286                    ContentType = string.Join(", ", rule.AllowedContentTypes)
 1287                };
 288            }
 289
 290            // application/x-www-form-urlencoded can't carry binary file parts; model values as string (or array of stri
 4291            urlEncodedProps[rule.Name] = CreateRuleSchema(isFile: false, allowMultiple: rule.AllowMultiple);
 292        }
 293
 2294        var multipartObject = new OpenApiSchema
 2295        {
 2296            Type = JsonSchemaType.Object,
 2297            Properties = multipartProps,
 2298            Required = required.Count > 0 ? required : null
 2299        };
 300
 2301        var urlEncodedObject = new OpenApiSchema
 2302        {
 2303            Type = JsonSchemaType.Object,
 2304            Properties = urlEncodedProps,
 2305            Required = required.Count > 0 ? required : null
 2306        };
 307
 2308        var content = new Dictionary<string, IOpenApiMediaType>(StringComparer.OrdinalIgnoreCase);
 12309        foreach (var ct in options.AllowedContentTypes)
 310        {
 4311            if (string.IsNullOrWhiteSpace(ct))
 312            {
 313                continue;
 314            }
 315
 4316            var isUrlEncoded = ct.Equals("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase);
 4317            var mediaType = new OpenApiMediaType
 4318            {
 4319                Schema = isUrlEncoded ? urlEncodedObject : multipartObject
 4320            };
 321
 4322            if (!isUrlEncoded && multipartEncoding.Count > 0)
 323            {
 1324                mediaType.Encoding = multipartEncoding;
 325            }
 326
 4327            content[ct] = mediaType;
 328        }
 329
 2330        return new OpenApiRequestBody
 2331        {
 2332            Required = true,
 2333            Description = "Form payload (multipart/* and/or application/x-www-form-urlencoded), parsed into $FormPayload
 2334            Content = content
 2335        };
 336    }
 337
 338    private static bool IsProbablyFileRule(KrFormPartRule rule)
 339    {
 8340        if (rule.StoreToDisk)
 341        {
 3342            return true;
 343        }
 344
 5345        if (rule.AllowedExtensions.Count > 0)
 346        {
 1347            return true;
 348        }
 349
 17350        foreach (var ct in rule.AllowedContentTypes)
 351        {
 5352            if (string.IsNullOrWhiteSpace(ct))
 353            {
 354                continue;
 355            }
 356
 5357            if (!ct.StartsWith("text/", StringComparison.OrdinalIgnoreCase)
 5358                && !ct.Equals("application/json", StringComparison.OrdinalIgnoreCase)
 5359                && !ct.Equals("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase))
 360            {
 1361                return true;
 362            }
 363        }
 364
 3365        return false;
 1366    }
 367
 368    private static IOpenApiSchema CreateRuleSchema(bool isFile, bool allowMultiple)
 369    {
 8370        var baseSchema = new OpenApiSchema
 8371        {
 8372            Type = JsonSchemaType.String,
 8373            Format = isFile ? "binary" : null
 8374        };
 375
 8376        if (!allowMultiple)
 377        {
 6378            return baseSchema;
 379        }
 380        // Returns an array
 2381        return new OpenApiSchema
 2382        {
 2383            Type = JsonSchemaType.Array,
 2384            Items = baseSchema
 2385        };
 386    }
 387
 388    private static MapRouteOptions MergeMapRouteOptions(MapRouteOptions parent, MapRouteOptions child)
 389    {
 1390        var mergedPattern = MergePattern(parent.Pattern, child.Pattern);
 391
 1392        var merged = new MapRouteOptions
 1393        {
 1394            Pattern = mergedPattern,
 1395            HttpVerbs = child.HttpVerbs is { Count: > 0 } ? child.HttpVerbs : parent.HttpVerbs,
 1396            CorsPolicy = !string.IsNullOrWhiteSpace(child.CorsPolicy) ? child.CorsPolicy : parent.CorsPolicy,
 1397            ThrowOnDuplicate = child.ThrowOnDuplicate || parent.ThrowOnDuplicate,
 1398            AllowAnonymous = child.AllowAnonymous || parent.AllowAnonymous,
 1399            ScriptCode = new LanguageOptions
 1400            {
 1401                Code = !string.IsNullOrWhiteSpace(child.ScriptCode.Code) ? child.ScriptCode.Code : parent.ScriptCode.Cod
 1402                Language = child.ScriptCode.Language != default ? child.ScriptCode.Language : parent.ScriptCode.Language
 1403                ExtraImports = MergeUnique(parent.ScriptCode.ExtraImports, child.ScriptCode.ExtraImports),
 1404                ExtraRefs = MergeRefs(parent.ScriptCode.ExtraRefs, child.ScriptCode.ExtraRefs),
 1405                Arguments = MergeArguments(parent.ScriptCode.Arguments, child.ScriptCode.Arguments)
 1406            },
 1407            OpenAPI = child.OpenAPI is { Count: > 0 } ? child.OpenAPI : parent.OpenAPI,
 1408        };
 409
 1410        var mergedSchemes = MergeUnique([.. parent.RequireSchemes], [.. child.RequireSchemes]);
 1411        merged.RequireSchemes.AddRange(mergedSchemes ?? []);
 412
 1413        var mergedPolicies = MergeUnique([.. parent.RequirePolicies], [.. child.RequirePolicies]);
 1414        merged.RequirePolicies.AddRange(mergedPolicies ?? []);
 415
 1416        return merged;
 417    }
 418
 419    private static string? MergePattern(string? parentPattern, string? childPattern)
 420    {
 2421        if (string.IsNullOrWhiteSpace(childPattern))
 422        {
 0423            return parentPattern;
 424        }
 425
 2426        if (string.IsNullOrWhiteSpace(parentPattern))
 427        {
 0428            return childPattern;
 429        }
 430
 431        // Returns a combined pattern, ensuring no double slashes.
 2432        return $"{parentPattern}/{childPattern}".Replace("//", "/", StringComparison.Ordinal);
 433    }
 434
 435    /// <summary>
 436    ///  Merges two string arrays into a single array with unique values.
 437    /// </summary>
 438    /// <param name="a">The first array of strings.</param>
 439    /// <param name="b">The second array of strings.</param>
 440    /// <returns>A merged array containing unique strings from both input arrays.</returns>
 441    private static string[]? MergeUnique(string[]? a, string[]? b)
 442    {
 4443        if (a is null && b is null)
 444        {
 1445            return null;
 446        }
 447
 3448        var set = new HashSet<string>(StringComparer.Ordinal);
 3449        AddNonEmptyValues(set, a);
 3450        AddNonEmptyValues(set, b);
 451
 3452        return set.Count == 0 ? [] : [.. set];
 453    }
 454
 455    /// <summary>
 456    /// Adds non-empty values from the source array into the destination set.
 457    /// </summary>
 458    /// <param name="destination">The target set to populate.</param>
 459    /// <param name="source">The source array of strings.</param>
 460    private static void AddNonEmptyValues(HashSet<string> destination, string[]? source)
 461    {
 6462        if (source is null)
 463        {
 0464            return;
 465        }
 466
 22467        foreach (var s in source)
 468        {
 5469            if (!string.IsNullOrWhiteSpace(s))
 470            {
 4471                _ = destination.Add(s);
 472            }
 473        }
 6474    }
 475
 476    private static System.Reflection.Assembly[]? MergeRefs(System.Reflection.Assembly[]? a, System.Reflection.Assembly[]
 477    {
 1478        if (b is null || b.Length == 0)
 479        {
 1480            return a;
 481        }
 482
 0483        if (a is null || a.Length == 0)
 484        {
 0485            return b;
 486        }
 487        // Returns
 0488        return [.. a, .. b];
 489    }
 490
 491    private static Dictionary<string, object?> MergeArguments(Dictionary<string, object?>? a, Dictionary<string, object?
 492    {
 2493        if (a is null || a.Count == 0)
 494        {
 1495            return b is null ? new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase) : new Dictionary<string
 496        }
 497
 1498        if (b is null || b.Count == 0)
 499        {
 0500            return new Dictionary<string, object?>(a, StringComparer.OrdinalIgnoreCase);
 501        }
 502
 1503        var m = new Dictionary<string, object?>(a, StringComparer.OrdinalIgnoreCase);
 6504        foreach (var (k, v) in b)
 505        {
 2506            m[k] = v;
 507        }
 508
 1509        return m;
 510    }
 511}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Hosting/KestrunHostMapExtensions.cs

#LineLine coverage
 1using System.Net;
 2using System.Text.RegularExpressions;
 3using Kestrun.Hosting.Options;
 4using Kestrun.Languages;
 5using Kestrun.Models;
 6using Kestrun.OpenApi;
 7using Kestrun.Runtime;
 8using Kestrun.Scripting;
 9using Kestrun.TBuilder;
 10using Kestrun.Utilities;
 11using Microsoft.AspNetCore.Antiforgery;
 12using Microsoft.AspNetCore.Authorization;
 13using Microsoft.Extensions.Options;
 14using Microsoft.OpenApi;
 15using Serilog.Events;
 16
 17namespace Kestrun.Hosting;
 18
 19/// <summary>
 20/// Provides extension methods for mapping routes and handlers to the KestrunHost.
 21/// </summary>
 22public static partial class KestrunHostMapExtensions
 23{
 24    /// <summary>
 25    /// Public utility facade for endpoint specification parsing. This provides a stable API surface
 26    /// over the internal helper logic used by host route constraint processing.
 27    /// </summary>
 28    public static class EndpointSpecParser
 29    {
 30        /// <summary>
 31        /// Parses an endpoint specification into host, port and optional HTTPS flag.
 32        /// </summary>
 33        /// <param name="spec">Specification string. See <see cref="TryParseEndpointSpec"/> for accepted formats.</param
 34        /// <param name="host">Resolved host when successful, otherwise empty string.</param>
 35        /// <param name="port">Resolved port when successful, otherwise 0.</param>
 36        /// <param name="https">True for https, false for http, null when unspecified (host:port form).</param>
 37        /// <returns><c>true</c> if parsing succeeds; otherwise <c>false</c>.</returns>
 38        public static bool TryParse(string spec, out string host, out int port, out bool? https)
 1639            => TryParseEndpointSpec(spec, out host, out port, out https);
 40    }
 41    /// <summary>
 42    /// Represents a delegate that handles a Kestrun request with the provided context.
 43    /// </summary>
 44    /// <param name="Context">The context for the Kestrun request.</param>
 45    /// <returns>A task representing the asynchronous operation.</returns>
 46    public delegate Task KestrunHandler(KestrunContext Context);
 47
 48    /// <summary>
 49    /// Adds a native route to the KestrunHost for the specified pattern and HTTP verb.
 50    /// </summary>
 51    /// <param name="host">The KestrunHost instance.</param>
 52    /// <param name="pattern">The route pattern.</param>
 53    /// <param name="httpVerb">The HTTP verb for the route.</param>
 54    /// <param name="handler">The handler to execute for the route.</param>
 55    /// <param name="requireSchemes">Optional array of authorization schemes required for the route.</param>
 56    /// <param name="map">The endpoint convention builder for further configuration.</param>
 57    /// <returns>The KestrunHost instance for chaining.</returns>
 58    public static KestrunHost AddMapRoute(this KestrunHost host, string pattern, HttpVerb httpVerb, KestrunHandler handl
 359    host.AddMapRoute(pattern: pattern, httpVerbs: [httpVerb], handler: handler, out map, requireSchemes: requireSchemes)
 60
 61    /// <summary>
 62    /// Adds a native route to the KestrunHost for the specified pattern and HTTP verbs.
 63    /// </summary>
 64    /// <param name="host">The KestrunHost instance.</param>
 65    /// <param name="pattern">The route pattern.</param>
 66    /// <param name="httpVerbs">The HTTP verbs for the route.</param>
 67    /// <param name="handler">The handler to execute for the route.</param>
 68    /// <param name="requireSchemes">Optional array of authorization schemes required for the route.</param>
 69    /// <param name="map">The endpoint convention builder for further configuration.</param>
 70    /// <returns>The KestrunHost instance for chaining.</returns>
 71    public static KestrunHost AddMapRoute(this KestrunHost host, string pattern, IEnumerable<HttpVerb> httpVerbs, Kestru
 72    out IEndpointConventionBuilder? map, List<string>? requireSchemes = null)
 73    {
 374        return host.AddMapRoute(new MapRouteOptions
 375        {
 376            Pattern = pattern,
 377            HttpVerbs = [.. httpVerbs],
 378            ScriptCode = new LanguageOptions
 379            {
 380                Language = ScriptLanguage.Native,
 381            },
 382            RequireSchemes = requireSchemes ?? [] // No authorization by default
 383        }, handler, out map);
 84    }
 85
 86    /// <summary>
 87    /// Adds a native route to the KestrunHost using the specified MapRouteOptions and handler.
 88    /// </summary>
 89    /// <param name="host">The KestrunHost instance.</param>
 90    /// <param name="options">The MapRouteOptions containing route configuration.</param>
 91    /// <param name="handler">The handler to execute for the route.</param>
 92    /// <param name="map">The endpoint convention builder for further configuration.</param>
 93    /// <returns>The KestrunHost instance for chaining.</returns>
 94    public static KestrunHost AddMapRoute(this KestrunHost host, MapRouteOptions options, KestrunHandler handler, out IE
 95    {
 1596        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 97        {
 198            host.Logger.Debug("AddMapRoute called with options={Options}", options);
 99        }
 100        // Ensure the WebApplication is initialized
 15101        if (host.App is null)
 102        {
 0103            throw new InvalidOperationException("WebApplication is not initialized. Call EnableConfiguration first.");
 104        }
 105
 106        // Validate options
 14107        if (string.IsNullOrWhiteSpace(options.Pattern))
 108        {
 0109            throw new ArgumentException("Pattern cannot be null or empty.", nameof(options.Pattern));
 110        }
 111
 14112        if (options.HttpVerbs.Count == 0)
 113        {
 1114            options.HttpVerbs = [HttpVerb.Get];
 115        }
 116
 14117        options.ScriptCode.Language = ScriptLanguage.Native;
 118
 28119        string[] methods = [.. options.HttpVerbs.Select(v => v.ToMethodString())];
 14120        map = host.App.MapMethods(options.Pattern, methods, async context =>
 14121         {
 14122             // 🔒 CSRF validation only for the current request when that verb is unsafe (unless disabled)
 5123             if (ShouldValidateCsrf(options, context))
 14124             {
 1125                 if (!await TryValidateAntiforgeryAsync(context))
 14126                 {
 0127                     return; // already responded 400
 14128                 }
 14129             }
 5130             var kestrunContext = new KestrunContext(host, context);
 5131             await handler(kestrunContext);
 5132             await kestrunContext.Response.ApplyTo(context.Response);
 19133         });
 134
 14135        host.AddMapOptions(map, options);
 14136        var registeredPattern = NormalizeCatchAllPattern(options.Pattern);
 56137        foreach (var method in options.HttpVerbs)
 138        {
 14139            host._registeredRoutes[(registeredPattern, method)] = options;
 140        }
 141
 14142        host.Logger.Information("Added native route: {Pattern} with methods: {Methods}", options.Pattern, string.Join(",
 143        // Add to the feature queue for later processing
 14144        host.FeatureQueue.Add(host => host.AddMapRoute(options));
 14145        return host;
 146    }
 147
 148    /// <summary>
 149    /// Adds a route to the KestrunHost that serves OpenAPI documents based on the provided options.
 150    /// </summary>
 151    /// <param name="host">The KestrunHost instance.</param>
 152    /// <param name="options">The OpenApiMapRouteOptions instance.</param>
 153    /// <returns>The KestrunHost instance for chaining.</returns>
 154    public static KestrunHost AddOpenApiMapRoute(this KestrunHost host, OpenApiMapRouteOptions options)
 155    {
 1156        ArgumentNullException.ThrowIfNull(options);
 1157        ArgumentNullException.ThrowIfNull(host);
 158
 159        // Validate options
 1160        return host.AddMapRoute(options.MapOptions, async context =>
 1161        {
 1162            // Extract parameters
 3163            var refresh = false;
 3164            var docId = options.DocId;
 1165            OpenApiSpecVersion specVersion;
 1166            // Try to get version and format from route values
 3167            var version = context.Request.RouteValues[options.VersionVarName]?.ToString() ?? options.DefaultVersion;
 3168            var format = context.Request.RouteValues[options.FormatVarName]?.ToString() ?? options.DefaultFormat;
 3169            if (context.Request.Query.TryGetValue(options.RefreshVarName, out var value))
 1170            {
 0171                _ = bool.TryParse(value, out refresh);
 1172            }
 1173            // Try to get version and format from route values
 1174            try
 1175            {
 3176                specVersion = OpenApiSpecVersionExtensions.ParseOpenApiSpecVersion(version);
 3177                if (format is not "json" and not "yaml")
 1178                {
 1179                    throw new InvalidOperationException($"Unsupported OpenAPI format requested: {format}");
 1180                }
 2181            }
 1182            catch
 1183            {
 1184                host.Logger.Warning("Invalid OpenAPI version or format requested: {Version}, {Format}", version, format)
 1185                context.Response.StatusCode = 404; // Not Found
 1186                return;
 1187            }
 1188            // Refresh the document if requested
 2189            if (refresh)
 1190            {
 0191                host.Logger.Information("Refreshing OpenAPI document cache as requested.");
 0192                var doc = host.OpenApiDocumentDescriptor[docId];
 0193                doc.GenerateDoc();
 1194            }
 1195            // Serve the document in the requested format
 2196            if (format == "json")
 1197            {
 1198                var json = host.OpenApiDocumentDescriptor[docId].ToJson(specVersion);
 1199                await context.Response.WriteTextResponseAsync(json, 200, "application/json");
 1200            }
 1201            else
 1202            {
 1203                var yml = host.OpenApiDocumentDescriptor[docId].ToYaml(specVersion);
 1204                await context.Response.WriteTextResponseAsync(yml, 200, "application/yaml");
 1205            }
 4206        }, out _);
 207    }
 208
 209    /// <summary>
 210    /// Adds a route to the KestrunHost that executes a script block for the specified HTTP verb and pattern.
 211    /// </summary>
 212    /// <param name="host">The KestrunHost instance.</param>
 213    /// <param name="pattern">The route pattern.</param>
 214    /// <param name="httpVerbs">The HTTP verb for the route.</param>
 215    /// <param name="scriptBlock">The script block to execute.</param>
 216    /// <param name="language">The scripting language to use (default is PowerShell).</param>
 217    /// <param name="requireSchemes">Optional array of authorization schemes required for the route.</param>
 218    /// <param name="arguments">Optional dictionary of arguments to pass to the script.</param>
 219    /// <returns>The KestrunHost instance for chaining.</returns>
 220    public static KestrunHost AddMapRoute(this KestrunHost host, string pattern, HttpVerb httpVerbs, string scriptBlock,
 221                                     List<string>? requireSchemes = null,
 222                                 Dictionary<string, object?>? arguments = null)
 223    {
 11224        arguments ??= [];
 11225        return host.AddMapRoute(new MapRouteOptions
 11226        {
 11227            Pattern = pattern,
 11228            HttpVerbs = [httpVerbs],
 11229            ScriptCode = new LanguageOptions
 11230            {
 11231                Code = scriptBlock,
 11232                Language = language,
 11233                Arguments = arguments ?? [] // No additional arguments by default
 11234            },
 11235            RequireSchemes = requireSchemes ?? [], // No authorization by default
 11236        });
 237    }
 238
 239    /// <summary>
 240    /// Adds a route to the KestrunHost that executes a script block for the specified HTTP verbs and pattern.
 241    /// </summary>
 242    /// <param name="host">The KestrunHost instance.</param>
 243    /// <param name="pattern">The route pattern.</param>
 244    /// <param name="httpVerbs">The HTTP verbs for the route.</param>
 245    /// <param name="scriptBlock">The script block to execute.</param>
 246    /// <param name="language">The scripting language to use (default is PowerShell).</param>
 247    /// <param name="requireSchemes">Optional array of authorization schemes required for the route.</param>
 248    /// <param name="arguments">Optional dictionary of arguments to pass to the script.</param>
 249    /// <returns>The KestrunHost instance for chaining.</returns>
 250    public static KestrunHost AddMapRoute(this KestrunHost host, string pattern,
 251                                IEnumerable<HttpVerb> httpVerbs,
 252                                string scriptBlock,
 253                                ScriptLanguage language = ScriptLanguage.PowerShell,
 254                                List<string>? requireSchemes = null,
 255                                 Dictionary<string, object?>? arguments = null)
 256    {
 1257        return host.AddMapRoute(new MapRouteOptions
 1258        {
 1259            Pattern = pattern,
 1260            HttpVerbs = [.. httpVerbs],
 1261            ScriptCode = new LanguageOptions
 1262            {
 1263                Code = scriptBlock,
 1264                Language = language,
 1265                Arguments = arguments ?? [] // No additional arguments by default
 1266            },
 1267            RequireSchemes = requireSchemes ?? [], // No authorization by default
 1268        });
 269    }
 270
 271    /// <summary>
 272    /// Adds a route to the KestrunHost using the specified MapRouteOptions.
 273    /// </summary>
 274    /// <param name="host">The KestrunHost instance.</param>
 275    /// <param name="options">The MapRouteOptions containing route configuration.</param>
 276    /// <returns>The KestrunHost instance for chaining.</returns>
 277    public static KestrunHost AddMapRoute(this KestrunHost host, MapRouteOptions options)
 278    {
 33279        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 280        {
 24281            host.Logger.Debug("AddMapRoute called with pattern={Pattern}, language={Language}, method={Methods}", option
 282        }
 33283        if (host.IsConfigured)
 284        {
 31285            _ = CreateMapRoute(host, options);
 286        }
 287        else
 288        {
 2289            _ = host.Use(app =>
 2290            {
 1291                _ = CreateMapRoute(host, options);
 3292            });
 293        }
 27294        return host; // for chaining
 295    }
 296
 297    /// <summary>
 298    /// Adds a route to the KestrunHost using the specified MapRouteOptions.
 299    /// </summary>
 300    /// <param name="host">The KestrunHost instance.</param>
 301    /// <param name="options">The MapRouteOptions containing route configuration.</param>
 302    /// <returns>The IEndpointConventionBuilder for the created route.</returns>
 303    private static IEndpointConventionBuilder CreateMapRoute(KestrunHost host, MapRouteOptions options)
 304    {
 32305        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 306        {
 23307            host.Logger.Debug("AddMapRoute called with pattern={Pattern}, language={Language}, method={Methods}", option
 308        }
 309
 310        try
 311        {
 312            // Validate options and get normalized route options
 32313            if (!ValidateRouteOptions(host, options, out var routeOptions))
 314            {
 0315                return null!; // Route already exists and should be skipped
 316            }
 317
 31318            var logger = host.Logger.ForContext("Route", routeOptions.Pattern);
 319
 320            // Compile the script once – return a RequestDelegate
 31321            var compiled = CompileScript(host, options.ScriptCode);
 322
 323            // Create and register the route
 31324            return CreateAndRegisterRoute(host, routeOptions, compiled);
 325        }
 0326        catch (CompilationErrorException ex)
 327        {
 328            // Log the detailed compilation errors
 0329            host.Logger.Error($"Failed to add route '{options.Pattern}' due to compilation errors:");
 0330            host.Logger.Error(ex.GetDetailedErrorMessage());
 331
 332            // Re-throw with additional context
 0333            throw new InvalidOperationException(
 0334                $"Failed to compile {options.ScriptCode.Language} script for route '{options.Pattern}'. {ex.GetErrors().
 0335                ex);
 336        }
 6337        catch (Exception ex)
 338        {
 6339            throw new InvalidOperationException(
 6340                $"Failed to add route '{options.Pattern}' with method '{string.Join(", ", options.HttpVerbs)}' using {op
 6341                ex);
 342        }
 26343    }
 344
 345    /// <summary>
 346    /// Validates the host and options for adding a map route.
 347    /// </summary>
 348    /// <param name="host">The KestrunHost instance.</param>
 349    /// <param name="options">The MapRouteOptions to validate.</param>
 350    /// <param name="routeOptions">The validated route options with defaults applied.</param>
 351    /// <returns>True if validation passes and route should be added; false if duplicate route should be skipped.</retur
 352    /// <exception cref="InvalidOperationException">Thrown when WebApplication is not initialized or route already exist
 353    /// <exception cref="ArgumentException">Thrown when required options are invalid.</exception>
 354    internal static bool ValidateRouteOptions(KestrunHost host, MapRouteOptions options, out MapRouteOptions routeOption
 355    {
 356        // Ensure the WebApplication is initialized
 41357        if (host.App is null)
 358        {
 0359            throw new InvalidOperationException("WebApplication is not initialized. Call EnableConfiguration first.");
 360        }
 361
 362        // Validate options
 40363        if (string.IsNullOrWhiteSpace(options.Pattern))
 364        {
 2365            throw new ArgumentException("Pattern cannot be null or empty.", nameof(options.Pattern));
 366        }
 367
 368        // Validate code
 38369        if (string.IsNullOrWhiteSpace(options.ScriptCode.Code))
 370        {
 2371            throw new ArgumentException("ScriptBlock cannot be null or empty.", nameof(options.ScriptCode.Code));
 372        }
 373
 36374        routeOptions = options;
 36375        if (options.HttpVerbs.Count == 0)
 376        {
 377            // If no HTTP verbs were specified, default to GET.
 2378            routeOptions.HttpVerbs = [HttpVerb.Get];
 379        }
 380
 36381        if (MapExists(host, routeOptions.Pattern, routeOptions.HttpVerbs))
 382        {
 3383            var msg = $"Route '{routeOptions.Pattern}' with method(s) {string.Join(", ", routeOptions.HttpVerbs)} alread
 3384            if (options.ThrowOnDuplicate)
 385            {
 2386                throw new InvalidOperationException(msg);
 387            }
 388
 1389            host.Logger.Warning(msg);
 1390            return false; // Skip this route
 391        }
 392
 33393        return true; // Continue with route creation
 394    }
 395
 396    /// <summary>
 397    /// Compiles the script code for the specified language.
 398    /// </summary>
 399    /// <param name="host">The KestrunHost instance.</param>
 400    /// <param name="options">The language options containing the script code and language.</param>
 401    /// <returns>A compiled RequestDelegate that can handle HTTP requests.</returns>
 402    /// <exception cref="NotSupportedException">Thrown when the script language is not supported.</exception>
 403    internal static RequestDelegate CompileScript(this KestrunHost host, LanguageOptions options)
 404    {
 38405        return options.Language switch
 38406        {
 2407            ScriptLanguage.PowerShell => PowerShellDelegateBuilder.Build(host, options.Code!, options.Arguments),
 33408            ScriptLanguage.CSharp => CSharpDelegateBuilder.Build(host, options.Code!, options.Arguments, options.ExtraIm
 2409            ScriptLanguage.VBNet => VBNetDelegateBuilder.Build(host, options.Code!, options.Arguments, options.ExtraImpo
 0410            ScriptLanguage.FSharp => FSharpDelegateBuilder.Build(host, options.Code!), // F# scripting not implemented
 0411            ScriptLanguage.Python => PyDelegateBuilder.Build(host, options.Code!),
 0412            ScriptLanguage.JavaScript => JScriptDelegateBuilder.Build(host, options.Code!),
 1413            _ => throw new NotSupportedException(options.Language.ToString())
 38414        };
 415    }
 416
 417    /// <summary>
 418    /// Creates and registers a route with the specified options and compiled handler.
 419    /// </summary>
 420    /// <param name="host">The KestrunHost instance.</param>
 421    /// <param name="routeOptions">The validated route options.</param>
 422    /// <param name="compiled">The compiled script delegate.</param>
 423    /// <returns>An IEndpointConventionBuilder for further configuration.</returns>
 424    internal static IEndpointConventionBuilder CreateAndRegisterRoute(KestrunHost host, MapRouteOptions routeOptions, Re
 425    {
 426        // Wrap with CSRF validation
 427        async Task handler(HttpContext ctx)
 428        {
 11429            if (ShouldValidateCsrf(routeOptions, ctx))
 430            {
 0431                if (!await TryValidateAntiforgeryAsync(ctx))
 432                {
 0433                    return; // already responded 400
 434                }
 435            }
 11436            await compiled(ctx);
 6437        }
 438
 33439        var mapPattern = NormalizeCatchAllPattern(routeOptions.Pattern!);
 69440        string[] methods = [.. routeOptions.HttpVerbs.Select(v => v.ToMethodString())];
 33441        var map = host.App!.MapMethods(mapPattern, methods, handler).WithLanguage(routeOptions.ScriptCode.Language);
 442
 33443        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 444        {
 24445            host.Logger.Debug("Mapped route: {Pattern} with methods: {Methods}", routeOptions.Pattern, string.Join(", ",
 446        }
 447
 33448        host.AddMapOptions(map, routeOptions);
 449
 450        // Register OpenAPI metadata for each verb
 118451        foreach (var method in routeOptions.HttpVerbs)
 452        {
 31453            if (routeOptions.OpenAPI.TryGetValue(method, out var value))
 454            {
 0455                ApplyOpenApiMetadata(host, map, value);
 456            }
 457            // Register the route to prevent duplicates
 31458            host._registeredRoutes[(mapPattern, method)] = routeOptions;
 459        }
 460
 28461        host.Logger.Information("Added route: {Pattern} with methods: {Methods}", mapPattern, string.Join(", ", methods)
 28462        return map;
 463    }
 464
 465    /// <summary>
 466    /// Adds additional mapping options to the route.
 467    /// </summary>
 468    /// <param name="host">The Kestrun host.</param>
 469    /// <param name="map">The endpoint convention builder.</param>
 470    /// <param name="options">The mapping options.</param>
 471    internal static void AddMapOptions(this KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 472    {
 51473        ApplyShortCircuit(host, map, options);
 51474        ApplyAnonymous(host, map, options);
 51475        DisableAntiforgery(host, map, options);
 51476        DisableResponseCompression(host, map, options);
 51477        ApplyRateLimiting(host, map, options);
 51478        ApplyAuthSchemes(host, map, options);
 50479        ApplyPolicies(host, map, options);
 49480        ApplyCors(host, map, options);
 49481        ApplyRequiredHost(host, map, options);
 46482        AddMetadata(host, map, options);
 46483    }
 484
 485    /// <summary>
 486    /// Tries to parse an endpoint specification string into its components: host, port, and HTTPS flag.
 487    /// </summary>
 488    /// <param name="spec">The endpoint specification string.</param>
 489    /// <param name="host">The host component.</param>
 490    /// <param name="port">The port component.</param>
 491    /// <param name="https">
 492    /// Indicates HTTPS (<c>true</c>) or HTTP (<c>false</c>) when the scheme is explicitly specified via a full URL.
 493    /// For host:port forms where no scheme information is available the value is <c>null</c>.
 494    /// </param>
 495    /// <returns>
 496    /// <c>true</c> if parsing succeeds; otherwise <c>false</c> and <paramref name="host"/> will be <c>string.Empty</c> 
 497    /// </returns>
 498    /// <remarks>
 499    /// Accepted formats (in priority order):
 500    /// <list type="bullet">
 501    /// <item><description>Full URL: <c>https://host:port</c>, <c>http://host:port</c>, IPv6 literal allowed in brackets
 502    /// <item><description>Bracketed IPv6 host &amp; port: <c>[::1]:5000</c>, <c>[2001:db8::1]:8080</c>.</description></
 503    /// <item><description>Host or IPv4 with port: <c>localhost:5000</c>, <c>127.0.0.1:8080</c>, <c>example.com:443</c>.
 504    /// </list>
 505    /// Unsupported / rejected examples: non http(s) schemes (e.g. <c>ftp://</c>), missing port in host:port form, empty
 506    /// </remarks>
 507    public static bool TryParseEndpointSpec(string spec, out string host, out int port, out bool? https)
 508    {
 171509        host = ""; port = 0; https = null;
 510
 57511        if (string.IsNullOrWhiteSpace(spec))
 512        {
 5513            return false;
 514        }
 515
 516        // 1. Try full URL form first
 52517        if (TryParseUrlSpec(spec, out host, out port, out https))
 518        {
 16519            return true;
 520        }
 521
 522        // 2. Bracketed IPv6 literal with port: [::1]:5000
 36523        if (TryParseBracketedIpv6Spec(spec, out host, out port))
 524        {
 4525            return true; // https stays null (not specified)
 526        }
 527
 528        // 3. Regular host:port (hostname, IPv4, or raw IPv6 w/out brackets not supported here)
 32529        if (TryParseHostPortSpec(spec, out host, out port))
 530        {
 12531            return true; // https stays null (not specified)
 532        }
 533
 534        // No match
 60535        host = ""; port = 0; https = null;
 20536        return false;
 537    }
 538
 539    /// <summary>
 540    /// Tries to parse a full URL endpoint specification.
 541    /// </summary>
 542    /// <param name="spec">The endpoint specification string.</param>
 543    /// <param name="host">The parsed host component.</param>
 544    /// <param name="port">The parsed port component.</param>
 545    /// <param name="https">The parsed HTTPS flag.</param>
 546    /// <returns><c>true</c> if parsing succeeded; otherwise <c>false</c>.</returns>
 547    private static bool TryParseUrlSpec(string spec, out string host, out int port, out bool? https)
 548    {
 156549        host = ""; port = 0; https = null;
 550        // Fast rejection for an explicitly empty port (e.g. "https://localhost:" or "http://[::1]:")
 551        // Uri.TryCreate will happily parse these and supply the default scheme port (80/443),
 552        // which would make us treat an intentionally empty port as a valid implicit port.
 553        // The accepted formats require either no colon at all (implicit default) OR a colon followed by digits.
 554        // Therefore pattern: scheme:// host-part : end-of-string (no digits after colon) should be rejected.
 52555        if (EmptyPortDetectionRegex().IsMatch(spec))
 556        {
 2557            return false;
 558        }
 50559        if (!Uri.TryCreate(spec, UriKind.Absolute, out var uri))
 560        {
 22561            return false;
 562        }
 28563        if (!(uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) ||
 28564              uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase)))
 565        {
 12566            return false; // Not http/https → let other parsers try
 567        }
 16568        if (uri.Authority.EndsWith(':'))
 569        {
 0570            return false; // reject empty port like https://localhost:
 571        }
 16572        host = uri.Host;
 16573        port = uri.Port;
 16574        https = uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)
 16575            ? true
 16576            : uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase)
 16577                ? false
 16578                : null;
 16579        return !string.IsNullOrWhiteSpace(host) && IsValidPort(port);
 580    }
 581
 582    /// <summary>
 583    /// Tries to parse a bracketed IPv6 endpoint specification.
 584    /// </summary>
 585    /// <param name="spec">The endpoint specification string.</param>
 586    /// <param name="host">The parsed host component.</param>
 587    /// <param name="port">The parsed port component.</param>
 588    /// <returns><c>true</c> if parsing succeeded; otherwise <c>false</c>.</returns>
 589    private static bool TryParseBracketedIpv6Spec(string spec, out string host, out int port)
 590    {
 72591        host = ""; port = 0;
 36592        var m = BracketedIpv6SpecMatcher().Match(spec);
 36593        if (!m.Success)
 594        {
 32595            return false;
 596        }
 4597        host = m.Groups[1].Value;
 4598        if (!int.TryParse(m.Groups[2].Value, out port) || !IsValidPort(port))
 599        {
 0600            host = ""; port = 0; return false;
 601        }
 4602        return !string.IsNullOrWhiteSpace(host);
 603    }
 604
 605    /// <summary>
 606    /// Tries to parse a host:port endpoint specification.
 607    /// </summary>
 608    /// <param name="spec">The endpoint specification string.</param>
 609    /// <param name="host">The parsed host component.</param>
 610    /// <param name="port">The parsed port component.</param>
 611    /// <returns><c>true</c> if parsing succeeded; otherwise <c>false</c>.</returns>
 612    private static bool TryParseHostPortSpec(string spec, out string host, out int port)
 613    {
 64614        host = ""; port = 0;
 32615        var m = HostPortSpecMatcher().Match(spec);
 32616        if (!m.Success)
 617        {
 17618            return false;
 619        }
 15620        host = m.Groups[1].Value;
 15621        if (!int.TryParse(m.Groups[2].Value, out port) || !IsValidPort(port))
 622        {
 9623            host = ""; port = 0; return false;
 624        }
 12625        return !string.IsNullOrWhiteSpace(host);
 626    }
 627    private const int MIN_PORT = 1;
 628    private const int MAX_PORT = 65535;
 629
 630    /// <summary>
 631    /// Validates that the port number is within the acceptable range (1-65535).
 632    /// </summary>
 633    /// <param name="port">The port number to validate.</param>
 634    /// <returns><c>true</c> if the port number is valid; otherwise, <c>false</c>.</returns>
 35635    private static bool IsValidPort(int port) => port is >= MIN_PORT and <= MAX_PORT;
 636
 637    /// <summary>
 638    /// Formats the host and port for use in RequireHost, adding brackets for IPv6 literals.
 639    /// </summary>
 640    /// <param name="host">The host component.</param>
 641    /// <param name="port">The port component.</param>
 642    /// <returns>The formatted host and port string.</returns>
 643    internal static string ToRequireHost(string host, int port) =>
 18644        IsIPv6Address(host) ? $"[{host}]:{port}" : $"{host}:{port}";
 645
 646    /// <summary>
 647    /// Determines if the given host string is an IPv6 address.
 648    /// </summary>
 649    /// <param name="host">The host string to check.</param>
 650    /// <returns>True if the host is an IPv6 address; otherwise, false.</returns>
 18651    private static bool IsIPv6Address(string host) => IPAddress.TryParse(host, out var ip) && ip.AddressFamily == System
 652
 653    /// <summary>
 654    /// Applies required hosts to the route based on the specified endpoints in the options.
 655    /// </summary>
 656    /// <param name="host">The Kestrun host.</param>
 657    /// <param name="map">The endpoint convention builder.</param>
 658    /// <param name="options">The mapping options.</param>
 659    /// <exception cref="ArgumentException">Thrown when the specified endpoints are invalid.</exception>
 660    internal static void ApplyRequiredHost(this KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions option
 661    {
 49662        if (options.Endpoints is not { Length: > 0 })
 663        {
 41664            return;
 665        }
 666
 8667        var listeners = host.Options.Listeners;
 8668        var require = new List<string>();
 8669        var errs = new List<string>();
 670
 38671        foreach (var spec in options.Endpoints)
 672        {
 11673            if (!TryParseEndpointSpec(spec, out var eh, out var ep, out var eHttps))
 674            {
 2675                errs.Add($"'{spec}' must be 'host:port' or 'http(s)://host:port'.");
 2676                continue;
 677            }
 678
 679            // Is the host a numeric IP?
 9680            var isNumericHost = IPAddress.TryParse(eh, out var endpointIp);
 681
 682            // Find a compatible listener: same port, scheme (if specified), and IP match if numeric host.
 9683            var match = listeners.FirstOrDefault(l =>
 19684                l.Port == ep &&
 19685                (eHttps is null || l.UseHttps == eHttps.Value) &&
 19686                (!isNumericHost ||
 19687                 l.IPAddress.Equals(endpointIp) ||
 19688                 l.IPAddress.Equals(IPAddress.Any) ||
 19689                 l.IPAddress.Equals(IPAddress.IPv6Any)));
 690
 9691            if (match is null)
 692            {
 2693                errs.Add($"'{spec}' doesn't match any configured listener. " +
 4694                         $"Known: {string.Join(", ", listeners.Select(l => l.ToString()))}");
 2695                continue;
 696            }
 697
 7698            require.Add(ToRequireHost(eh, ep));
 699        }
 700
 8701        if (errs.Count > 0)
 702        {
 3703            throw new InvalidOperationException("Invalid Endpoints:" + Environment.NewLine + "  - " + string.Join(Enviro
 704        }
 5705        if (require.Count > 0)
 706        {
 5707            host.Logger.Verbose("Applying required hosts: {RequiredHosts} to route: {Pattern}",
 5708                string.Join(", ", require), options.Pattern);
 5709            _ = map.RequireHost([.. require]);
 710        }
 5711    }
 712
 713    /// <summary>
 714    /// Applies the same route conventions used by the AddMapRoute helpers to an arbitrary endpoint.
 715    /// </summary>
 716    /// <param name="host">The Kestrun host used for validation (auth schemes/policies).</param>
 717    /// <param name="builder">The endpoint convention builder to decorate.</param>
 718    /// <param name="configure">Delegate to configure a fresh <see cref="MapRouteOptions"/> instance. Only applicable pr
 719    /// <remarks>
 720    /// This is useful when you map endpoints manually via <c>app.MapGet</c>/<c>MapPost</c> and still want consistent be
 721    /// (auth, CORS, rate limiting, antiforgery disable, OpenAPI metadata, short-circuiting) without re-implementing log
 722    /// Validation notes:
 723    ///  - Pattern, Code are ignored if not relevant.
 724    ///  - Authentication schemes and policies are validated against the host registry.
 725    ///  - OpenAPI metadata is applied only when non-empty.
 726    /// </remarks>
 727    /// <returns>The original <paramref name="builder"/> for fluent chaining.</returns>
 728    public static IEndpointConventionBuilder ApplyKestrunConventions(this KestrunHost host, IEndpointConventionBuilder b
 729    {
 1730        ArgumentNullException.ThrowIfNull(host);
 1731        ArgumentNullException.ThrowIfNull(builder);
 1732        ArgumentNullException.ThrowIfNull(configure);
 733
 734        // Start with an empty options record (only convention-related fields will matter)
 1735        var options = new MapRouteOptions
 1736        {
 1737            Pattern = string.Empty,
 1738            HttpVerbs = [],
 1739            ScriptCode = new LanguageOptions
 1740            {
 1741                Language = ScriptLanguage.Native,
 1742                Code = string.Empty
 1743            }
 1744        };
 1745        configure(options);
 746
 747        // Reuse internal helper (kept internal to avoid accidental misuse) for actual application
 1748        host.AddMapOptions(builder, options);
 1749        return builder;
 750    }
 751    /// <summary>
 752    /// Adds metadata to the route from the script parameters.
 753    /// </summary>
 754    /// <param name="host">The Kestrun host.</param>
 755    /// <param name="map">The endpoint convention builder.</param>
 756    /// <param name="options">The mapping options.</param>
 757    private static void AddMetadata(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 758    {
 46759        if (options.ScriptCode is null || options.ScriptCode.Parameters is null || options.ScriptCode.Parameters.Count =
 760        {
 46761            return;
 762        }
 763
 0764        host.Logger.Verbose("Adding metadata to route: {Pattern}", options.Pattern);
 0765        _ = map.WithMetadata(options.ScriptCode.Parameters);
 0766        options.DefaultResponseContentType ??= new Dictionary<string, ICollection<ContentTypeWithSchema>>(host.Options.D
 0767        if (options.DefaultResponseContentType != null && options.DefaultResponseContentType.Count > 0)
 768        {
 0769            _ = map.WithMetadata(new DefaultResponseContentType(options.DefaultResponseContentType));
 770        }
 0771    }
 772    /// <summary>
 773    /// Applies short-circuiting behavior to the route.
 774    /// </summary>
 775    /// <param name="host">The Kestrun host.</param>
 776    /// <param name="map">The endpoint convention builder.</param>
 777    /// <param name="options">The mapping options.</param>
 778    private static void ApplyShortCircuit(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 779    {
 51780        if (!options.ShortCircuit)
 781        {
 51782            return;
 783        }
 784
 0785        host.Logger.Verbose("Short-circuiting route: {Pattern} with status code: {StatusCode}", options.Pattern, options
 0786        if (options.ShortCircuitStatusCode is null)
 787        {
 0788            throw new ArgumentException("ShortCircuitStatusCode must be set if ShortCircuit is true.", nameof(options.Sh
 789        }
 790
 0791        _ = map.ShortCircuit(options.ShortCircuitStatusCode);
 0792    }
 793
 794    /// <summary>
 795    /// Applies anonymous access behavior to the route.
 796    /// </summary>
 797    /// <param name="host">The Kestrun host.</param>
 798    /// <param name="map">The endpoint convention builder.</param>
 799    /// <param name="options">The mapping options.</param>
 800    private static void ApplyAnonymous(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 801    {
 51802        if (options.AllowAnonymous)
 803        {
 2804            host.Logger.Verbose("Allowing anonymous access for route: {Pattern}", options.Pattern);
 2805            _ = map.AllowAnonymous();
 806        }
 807        else
 808        {
 49809            host.Logger.Debug("No anonymous access allowed for route: {Pattern}", options.Pattern);
 810        }
 49811    }
 812
 813    /// <summary>
 814    /// Disables anti-forgery behavior to the route.
 815    /// </summary>
 816    /// <param name="host">The Kestrun host.</param>
 817    /// <param name="map">The endpoint convention builder.</param>
 818    /// <param name="options">The mapping options.</param>
 819    private static void DisableAntiforgery(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 820    {
 51821        if (!options.DisableAntiforgery)
 822        {
 50823            return;
 824        }
 825
 1826        _ = map.DisableAntiforgery();
 1827        host.Logger.Verbose("CSRF protection disabled for route: {Pattern}", options.Pattern);
 1828    }
 829
 830    /// <summary>
 831    /// Disables response compression for the route.
 832    /// </summary>
 833    /// <param name="host">The Kestrun host.</param>
 834    /// <param name="map">The endpoint convention builder.</param>
 835    /// <param name="options">The mapping options.</param>
 836    private static void DisableResponseCompression(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions opt
 837    {
 51838        if (!options.DisableResponseCompression)
 839        {
 50840            return;
 841        }
 842
 1843        _ = map.DisableResponseCompression();
 1844        host.Logger.Verbose("Response compression disabled for route: {Pattern}", options.Pattern);
 1845    }
 846    /// <summary>
 847    /// Applies rate limiting behavior to the route.
 848    /// </summary>
 849    /// <param name="host">The Kestrun host.</param>
 850    /// <param name="map">The endpoint convention builder.</param>
 851    /// <param name="options">The mapping options.</param>
 852    private static void ApplyRateLimiting(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 853    {
 51854        if (string.IsNullOrWhiteSpace(options.RateLimitPolicyName))
 855        {
 51856            return;
 857        }
 858
 0859        host.Logger.Verbose("Applying rate limit policy: {RateLimitPolicyName} to route: {Pattern}", options.RateLimitPo
 0860        _ = map.RequireRateLimiting(options.RateLimitPolicyName);
 0861    }
 862
 863    /// <summary>
 864    /// Applies authentication schemes to the route.
 865    /// </summary>
 866    /// <param name="host">The Kestrun host.</param>
 867    /// <param name="map">The endpoint convention builder.</param>
 868    /// <param name="options">The mapping options.</param>
 869    private static void ApplyAuthSchemes(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 870    {
 51871        if (options.RequireSchemes is not null && options.RequireSchemes.Count != 0)
 872        {
 7873            foreach (var schema in options.RequireSchemes)
 874            {
 2875                if (!host.HasAuthScheme(schema))
 876                {
 1877                    throw new ArgumentException($"Authentication scheme '{schema}' is not registered.", nameof(options.R
 878                }
 879            }
 1880            host.Logger.Verbose("Requiring authorization for route: {Pattern} with policies: {Policies}", options.Patter
 1881            _ = map.RequireAuthorization(new AuthorizeAttribute
 1882            {
 1883                AuthenticationSchemes = string.Join(',', options.RequireSchemes)
 1884            });
 885        }
 886        else
 887        {
 49888            host.Logger.Debug("No authorization required for route: {Pattern}", options.Pattern);
 889        }
 49890    }
 891
 892    /// <summary>
 893    /// Applies authorization policies to the route.
 894    /// </summary>
 895    /// <param name="host">The Kestrun host.</param>
 896    /// <param name="map">The endpoint convention builder.</param>
 897    /// <param name="options">The mapping options.</param>
 898    private static void ApplyPolicies(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 899    {
 50900        if (options.RequirePolicies is not null && options.RequirePolicies.Count != 0)
 901        {
 7902            foreach (var policy in options.RequirePolicies)
 903            {
 2904                if (!host.HasAuthPolicy(policy))
 905                {
 1906                    throw new ArgumentException($"Authorization policy '{policy}' is not registered.", nameof(options.Re
 907                }
 908            }
 1909            _ = map.RequireAuthorization(options.RequirePolicies.ToArray());
 910        }
 911        else
 912        {
 48913            host.Logger.Debug("No authorization policies required for route: {Pattern}", options.Pattern);
 914        }
 48915    }
 916    /// <summary>
 917    /// Applies CORS behavior to the route.
 918    /// </summary>
 919    /// <param name="host">The Kestrun host.</param>
 920    /// <param name="map">The endpoint convention builder.</param>
 921    /// <param name="options">The mapping options.</param>
 922    private static void ApplyCors(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 923    {
 49924        if (!string.IsNullOrWhiteSpace(options.CorsPolicy))
 925        {
 0926            if (!host.DefinedCorsPolicyNames.Contains(options.CorsPolicy))
 927            {
 0928                throw new ArgumentException($"CORS policy '{options.CorsPolicy}' is not registered.");
 929            }
 0930            host.Logger.Verbose("Applying CORS policy: {CorsPolicy} to route: {Pattern}", options.CorsPolicy, options.Pa
 0931            _ = map.RequireCors(options.CorsPolicy);
 0932            return;
 933        }
 934        // No per-route policy requested.
 49935        if (host.CorsPolicyDefined)
 936        {
 0937            host.Logger.Verbose("No per-route CORS policy set for route: {Pattern}; default CORS policy will apply.", op
 938        }
 939        else
 940        {
 49941            host.Logger.Debug("No CORS policy configured for route: {Pattern}", options.Pattern);
 942        }
 49943    }
 944
 945    /// <summary>
 946    /// Applies OpenAPI metadata to the route.
 947    /// </summary>
 948    /// <param name="host">The Kestrun host.</param>
 949    /// <param name="map">The endpoint convention builder.</param>
 950    /// <param name="openAPI">The OpenAPI metadata.</param>
 951    private static void ApplyOpenApiMetadata(KestrunHost host, IEndpointConventionBuilder map, OpenAPIPathMetadata openA
 952    {
 0953        if (!string.IsNullOrEmpty(openAPI.OperationId))
 954        {
 0955            host.Logger.Verbose("Adding OpenAPI metadata for route: {Pattern} with OperationId: {OperationId}", openAPI.
 0956            _ = map.WithName(openAPI.OperationId);
 957        }
 958
 0959        if (!string.IsNullOrWhiteSpace(openAPI.Summary))
 960        {
 0961            host.Logger.Verbose("Adding OpenAPI summary for route: {Pattern} with Summary: {Summary}", openAPI.Pattern, 
 0962            _ = map.WithSummary(openAPI.Summary);
 963        }
 964
 0965        if (!string.IsNullOrWhiteSpace(openAPI.Description))
 966        {
 0967            host.Logger.Verbose("Adding OpenAPI description for route: {Pattern} with Description: {Description}", openA
 0968            _ = map.WithDescription(openAPI.Description);
 969        }
 970
 0971        if (openAPI.Tags.Count > 0)
 972        {
 0973            host.Logger.Verbose("Adding OpenAPI tags for route: {Pattern} with Tags: {Tags}", openAPI.Pattern, string.Jo
 0974            _ = map.WithTags([.. openAPI.Tags]);
 975        }
 0976    }
 977
 978    /// <summary>
 979    /// Adds an HTML template route to the KestrunHost for the specified pattern and HTML file path.
 980    /// </summary>
 981    /// <param name="host">The KestrunHost instance.</param>
 982    /// <param name="pattern">The route pattern.</param>
 983    /// <param name="htmlFilePath">The path to the HTML template file.</param>
 984    /// <param name="requireSchemes">Optional array of authorization schemes required for the route.</param>
 985    /// <returns>An IEndpointConventionBuilder for further configuration.</returns>
 986    public static IEndpointConventionBuilder AddHtmlTemplateRoute(this KestrunHost host, string pattern, string htmlFile
 987    {
 0988        return host.AddHtmlTemplateRoute(new MapRouteOptions
 0989        {
 0990            Pattern = pattern,
 0991            HttpVerbs = [HttpVerb.Get],
 0992            RequireSchemes = requireSchemes ?? [] // No authorization by default
 0993        }, htmlFilePath);
 994    }
 995
 996    /// <summary>
 997    /// Adds an HTML template route to the KestrunHost using the specified MapRouteOptions and HTML file path.
 998    /// </summary>
 999    /// <param name="host">The KestrunHost instance.</param>
 1000    /// <param name="options">The MapRouteOptions containing route configuration.</param>
 1001    /// <param name="htmlFilePath">The path to the HTML template file.</param>
 1002    /// <returns>An IEndpointConventionBuilder for further configuration.</returns>
 1003    public static IEndpointConventionBuilder AddHtmlTemplateRoute(this KestrunHost host, MapRouteOptions options, string
 1004    {
 31005        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 1006        {
 21007            host.Logger.Debug("Adding HTML template route: {Pattern}", options.Pattern);
 1008        }
 1009
 31010        if (options.HttpVerbs.Count != 0 &&
 31011            (options.HttpVerbs.Count > 1 || options.HttpVerbs.First() != HttpVerb.Get))
 1012        {
 11013            host.Logger.Error("HTML template routes only support GET requests. Provided HTTP verbs: {HttpVerbs}", string
 11014            throw new ArgumentException("HTML template routes only support GET requests.", nameof(options.HttpVerbs));
 1015        }
 21016        if (string.IsNullOrWhiteSpace(htmlFilePath) || !File.Exists(htmlFilePath))
 1017        {
 11018            host.Logger.Error("HTML file path is null, empty, or does not exist: {HtmlFilePath}", htmlFilePath);
 11019            throw new FileNotFoundException("HTML file not found.", htmlFilePath);
 1020        }
 1021
 11022        if (string.IsNullOrWhiteSpace(options.Pattern))
 1023        {
 01024            host.Logger.Error("Pattern cannot be null or empty.");
 01025            throw new ArgumentException("Pattern cannot be null or empty.", nameof(options.Pattern));
 1026        }
 1027
 11028        _ = host.AddMapRoute(options.Pattern, HttpVerb.Get, async (ctx) =>
 11029          {
 11030              // ② Build your variables map
 01031              var vars = new Dictionary<string, object?>();
 01032              _ = VariablesMap.GetVariablesMap(ctx, ref vars);
 11033
 01034              await ctx.Response.WriteHtmlResponseFromFileAsync(htmlFilePath, vars, ctx.Response.StatusCode);
 11035          }, out var map);
 11036        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 1037        {
 11038            host.Logger.Debug("Mapped HTML template route: {Pattern} to file: {HtmlFilePath}", options.Pattern, htmlFile
 1039        }
 11040        if (map is null)
 1041        {
 01042            throw new InvalidOperationException("Failed to create HTML template route.");
 1043        }
 11044        AddMapOptions(host, map, options);
 11045        return map;
 1046    }
 1047
 1048    /// <summary>
 1049    /// Adds a Swagger UI route to the KestrunHost for the specified pattern and OpenAPI endpoint.
 1050    /// </summary>
 1051    /// <param name="host">The KestrunHost instance.</param>
 1052    /// <param name="options">The MapRouteOptions containing route configuration.</param>
 1053    /// <param name="openApiEndpoint">The URI of the OpenAPI endpoint.</param>
 1054    /// <returns>An IEndpointConventionBuilder for further configuration.</returns>
 1055    /// <exception cref="ArgumentException">Thrown when the provided options are invalid.</exception>
 1056    /// <exception cref="InvalidOperationException">Thrown when the Swagger UI route cannot be created.</exception>
 1057    public static IEndpointConventionBuilder AddSwaggerUiRoute(
 1058        this KestrunHost host,
 1059        MapRouteOptions options,
 1060        Uri openApiEndpoint)
 1061    {
 21062        return AddOpenApiUiRoute(
 21063            host,
 21064            options,
 21065            openApiEndpoint,
 21066            uiName: "Swagger",
 21067            defaultPattern: "/docs/swagger",
 21068            resourceName: "Kestrun.Assets.swagger-ui.html");
 1069    }
 1070
 1071    /// <summary>
 1072    /// Adds a Redoc UI route to the KestrunHost for the specified pattern and OpenAPI endpoint.
 1073    /// </summary>
 1074    /// <param name="host">The KestrunHost instance.</param>
 1075    /// <param name="options">The route mapping options.</param>
 1076    /// <param name="openApiEndpoint">The OpenAPI endpoint URI.</param>
 1077    /// <returns>An IEndpointConventionBuilder for the mapped route.</returns>
 1078    /// <exception cref="ArgumentException">Thrown when the provided options are invalid.</exception>
 1079    /// <exception cref="InvalidOperationException">Thrown when the Redoc UI route cannot be created.</exception>
 1080    public static IEndpointConventionBuilder AddRedocUiRoute(
 1081        this KestrunHost host,
 1082        MapRouteOptions options,
 1083        Uri openApiEndpoint)
 1084    {
 01085        return AddOpenApiUiRoute(
 01086            host,
 01087            options,
 01088            openApiEndpoint,
 01089            uiName: "Redoc",
 01090            defaultPattern: "/docs/redoc",
 01091            resourceName: "Kestrun.Assets.redoc-ui.html");
 1092    }
 1093
 1094    /// <summary>
 1095    /// Adds a Scalar UI route to the KestrunHost for the specified pattern and OpenAPI endpoint.
 1096    /// </summary>
 1097    /// <param name="host">The KestrunHost instance.</param>
 1098    /// <param name="options">The route mapping options.</param>
 1099    /// <param name="openApiEndpoint">The OpenAPI endpoint URI.</param>
 1100    /// <returns>An IEndpointConventionBuilder for the mapped route.</returns>
 1101    /// <exception cref="ArgumentException">Thrown when the provided options are invalid.</exception>
 1102    /// <exception cref="InvalidOperationException">Thrown when the Scalar UI route cannot be created.</exception>
 1103    public static IEndpointConventionBuilder AddScalarUiRoute(
 1104        this KestrunHost host,
 1105        MapRouteOptions options,
 1106        Uri openApiEndpoint)
 1107    {
 01108        return AddOpenApiUiRoute(
 01109            host,
 01110            options,
 01111            openApiEndpoint,
 01112            uiName: "Scalar",
 01113            defaultPattern: "/docs/scalar",
 01114            resourceName: "Kestrun.Assets.scalar.html");
 1115    }
 1116
 1117    /// <summary>
 1118    /// Adds a RapiDoc UI route to the KestrunHost for the specified pattern and OpenAPI endpoint.
 1119    /// </summary>
 1120    /// <param name="host">The KestrunHost instance.</param>
 1121    /// <param name="options">The route mapping options.</param>
 1122    /// <param name="openApiEndpoint">The OpenAPI endpoint URI.</param>
 1123    /// <returns>An IEndpointConventionBuilder for the mapped route.</returns>
 1124    public static IEndpointConventionBuilder AddRapiDocUiRoute(
 1125       this KestrunHost host,
 1126       MapRouteOptions options,
 1127       Uri openApiEndpoint)
 1128    {
 01129        return AddOpenApiUiRoute(
 01130            host,
 01131            options,
 01132            openApiEndpoint,
 01133            uiName: "RapiDoc",
 01134            defaultPattern: "/docs/rapidoc",
 01135            resourceName: "Kestrun.Assets.rapidoc.html");
 1136    }
 1137
 1138    /// <summary>
 1139    /// Adds an Elements UI route to the KestrunHost for the specified pattern and OpenAPI endpoint.
 1140    /// </summary>
 1141    /// <param name="host">The KestrunHost instance.</param>
 1142    /// <param name="options">The route mapping options.</param>
 1143    /// <param name="openApiEndpoint">The OpenAPI endpoint URI.</param>
 1144    /// <returns>An IEndpointConventionBuilder for the mapped route.</returns>
 1145    public static IEndpointConventionBuilder AddElementsUiRoute(
 1146      this KestrunHost host,
 1147      MapRouteOptions options,
 1148      Uri openApiEndpoint)
 1149    {
 01150        return AddOpenApiUiRoute(
 01151            host,
 01152            options,
 01153            openApiEndpoint,
 01154            uiName: "Elements",
 01155            defaultPattern: "/docs/elements",
 01156            resourceName: "Kestrun.Assets.elements.html");
 1157    }
 1158
 1159    /// <summary>
 1160    /// Adds an OpenAPI UI route to the KestrunHost for the specified pattern and OpenAPI endpoint.
 1161    /// </summary>
 1162    /// <param name="host">The KestrunHost instance.</param>
 1163    /// <param name="options">The route mapping options.</param>
 1164    /// <param name="openApiEndpoint">The OpenAPI endpoint URI.</param>
 1165    /// <param name="uiName">The name of the UI.</param>
 1166    /// <param name="defaultPattern">The default route pattern.</param>
 1167    /// <param name="resourceName">The embedded resource name.</param>
 1168    /// <returns>The endpoint convention builder for the mapped route.</returns>
 1169    /// <exception cref="ArgumentException">Thrown when the provided options are invalid.</exception>
 1170    /// <exception cref="InvalidOperationException">Thrown when the OpenAPI UI route cannot be created.</exception>
 1171    private static IEndpointConventionBuilder AddOpenApiUiRoute(
 1172        KestrunHost host,
 1173        MapRouteOptions options,
 1174        Uri openApiEndpoint,
 1175        string uiName,
 1176        string defaultPattern,
 1177        string resourceName)
 1178    {
 21179        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 1180        {
 01181            host.Logger.Debug(
 01182                "Adding {UiName} UI route: {Pattern} for OpenAPI endpoint: {OpenApiEndpoint}",
 01183                uiName,
 01184                options.Pattern,
 01185                openApiEndpoint);
 1186        }
 1187
 21188        if (options.HttpVerbs.Count != 0 &&
 21189            (options.HttpVerbs.Count > 1 || options.HttpVerbs.First() != HttpVerb.Get))
 1190        {
 11191            host.Logger.Error(
 11192                "{UiName} UI routes only support GET requests. Provided HTTP verbs: {HttpVerbs}",
 11193                uiName,
 11194                string.Join(", ", options.HttpVerbs));
 1195
 11196            throw new ArgumentException(
 11197                $"{uiName} UI routes only support GET requests.",
 11198                nameof(options.HttpVerbs));
 1199        }
 1200
 1201        // Set default pattern if not provided
 11202        if (string.IsNullOrWhiteSpace(options.Pattern))
 1203        {
 11204            options.Pattern = defaultPattern;
 1205        }
 1206
 1207        // Load embedded UI HTML
 11208        var map = AddHtmlRouteFromEmbeddedResource(host, options.Pattern, openApiEndpoint, resourceName);
 1209
 11210        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 1211        {
 01212            host.Logger.Debug(
 01213                "Mapped {UiName} UI route: {Pattern} for OpenAPI endpoint: {OpenApiEndpoint}",
 01214                uiName,
 01215                options.Pattern,
 01216                openApiEndpoint);
 1217        }
 1218
 11219        if (map is null)
 1220        {
 01221            throw new InvalidOperationException($"Failed to create {uiName} UI route.");
 1222        }
 1223
 11224        AddMapOptions(host, map, options);
 11225        return map;
 1226    }
 1227
 1228    /// <summary>
 1229    /// Add a HTML route from an embedded resource.
 1230    /// </summary>
 1231    /// <param name="host">The KestrunHost instance.</param>
 1232    /// <param name="pattern">The route pattern.</param>
 1233    /// <param name="openApiEndpoint">The OpenAPI endpoint URI.</param>
 1234    /// <param name="embeddedResource">The embedded resource name.</param>
 1235    /// <exception cref="InvalidOperationException"></exception>
 1236    private static IEndpointConventionBuilder? AddHtmlRouteFromEmbeddedResource(KestrunHost host, string pattern, Uri op
 1237    {
 11238        _ = host.AddMapRoute(pattern: pattern, httpVerb: HttpVerb.Get, async (ctx) =>
 11239          {
 11240              var asm = typeof(KestrunHostMapExtensions).Assembly;
 11241              using var stream = asm.GetManifestResourceStream(embeddedResource)
 11242                    ?? throw new InvalidOperationException($"Embedded HTML resource not found: {embeddedResource}");
 11243
 11244              using var ms = new MemoryStream();
 11245              stream.CopyTo(ms);
 11246              var htmlBuffer = ms.ToArray();
 11247              ctx.Response.ContentType = "text/html; charset=utf-8";
 11248              await ctx.Response.WriteHtmlResponseAsync(htmlBuffer, new Dictionary<string, object?>
 11249              {
 11250                  { "OPENAPI_ENDPOINT", openApiEndpoint.ToString() }
 11251              }, ctx.Response.StatusCode);
 21252          }, out var map);
 11253        return map;
 1254    }
 1255
 1256    /// <summary>
 1257    /// Checks if a route with the specified pattern and optional HTTP method exists in the KestrunHost.
 1258    /// </summary>
 1259    /// <param name="host">The KestrunHost instance.</param>
 1260    /// <param name="pattern">The route pattern to check.</param>
 1261    /// <param name="verbs">The optional HTTP method to check for the route.</param>
 1262    /// <returns>True if the route exists; otherwise, false.</returns>
 1263    public static bool MapExists(this KestrunHost host, string pattern, IEnumerable<HttpVerb> verbs)
 1264    {
 401265        var normalizedPattern = NormalizeCatchAllPattern(pattern);
 821266        var methodSet = verbs.Select(v => v.ToMethodString()).ToHashSet(StringComparer.OrdinalIgnoreCase);
 401267        return host._registeredRoutes.Keys
 111268            .Where(k => string.Equals(k.Pattern, normalizedPattern, StringComparison.OrdinalIgnoreCase))
 491269            .Any(k => methodSet.Contains(k.Method.ToMethodString()));
 1270    }
 1271
 1272    /// <summary>
 1273    /// Checks if a route with the specified pattern and optional HTTP method exists in the KestrunHost.
 1274    /// </summary>
 1275    /// <param name="host">The KestrunHost instance.</param>
 1276    /// <param name="pattern">The route pattern to check.</param>
 1277    /// <param name="verb">The optional HTTP method to check for the route.</param>
 1278    /// <returns>True if the route exists; otherwise, false.</returns>
 1279    public static bool MapExists(this KestrunHost host, string pattern, HttpVerb verb)
 1280    {
 91281        var normalizedPattern = NormalizeCatchAllPattern(pattern);
 91282        return host._registeredRoutes.ContainsKey((normalizedPattern, verb));
 1283    }
 1284
 1285    /// <summary>
 1286    /// Retrieves the <see cref="MapRouteOptions"/> associated with a given route pattern and HTTP verb, if registered.
 1287    /// </summary>
 1288    /// <param name="host">The <see cref="KestrunHost"/> instance to search for registered routes.</param>
 1289    /// <param name="pattern">The route pattern to look up (e.g. <c>"/hello"</c>).</param>
 1290    /// <param name="verb">The HTTP verb to match (e.g. <see cref="HttpVerb.Get"/>).</param>
 1291    /// <returns>
 1292    /// The <see cref="MapRouteOptions"/> instance for the specified route if found; otherwise, <c>null</c>.
 1293    /// </returns>
 1294    /// <remarks>
 1295    /// This method checks the internal route registry and returns the route options if the pattern and verb
 1296    /// combination was previously added via <c>AddMapRoute</c>.
 1297    /// This lookup is case-insensitive for both the pattern and method.
 1298    /// </remarks>
 1299    /// <example>
 1300    /// <code>
 1301    /// var options = host.GetMapRouteOptions("/hello", HttpVerb.Get);
 1302    /// if (options != null)
 1303    /// {
 1304    ///     Console.WriteLine($"Route language: {options.Language}");
 1305    /// }
 1306    /// </code>
 1307    /// </example>
 1308    public static MapRouteOptions? GetMapRouteOptions(this KestrunHost host, string pattern, HttpVerb verb)
 1309    {
 51310        var normalizedPattern = NormalizeCatchAllPattern(pattern);
 51311        return host._registeredRoutes.TryGetValue((normalizedPattern, verb), out var options)
 51312            ? options
 51313            : null;
 1314    }
 1315
 1316    /// <summary>
 1317    /// Normalizes catch-all parameters that use "{**name}" into "{*name}" for ASP.NET Core routing.
 1318    /// </summary>
 1319    /// <param name="pattern">The route pattern to normalize.</param>
 1320    /// <returns>The normalized route pattern.</returns>
 1321    private static string NormalizeCatchAllPattern(string pattern)
 1322    {
 1011323        return string.IsNullOrWhiteSpace(pattern)
 1011324            ? pattern
 1011325            : pattern.Replace("{**", "{*", StringComparison.Ordinal);
 1326    }
 1327
 1328    /// <summary>
 1329    /// Adds a GET endpoint that issues the antiforgery cookie and returns a JSON payload:
 1330    /// { token: "...", headerName: "X-CSRF-TOKEN" }.
 1331    /// The endpoint itself is exempt from antiforgery validation.
 1332    /// </summary>
 1333    /// <param name="host">The KestrunHost instance.</param>
 1334    /// <param name="pattern">The route path to expose (default "/csrf-token").</param>
 1335    /// <returns>IEndpointConventionBuilder for further configuration.</returns>
 1336    public static IEndpointConventionBuilder AddAntiforgeryTokenRoute(
 1337    this KestrunHost host,
 1338    string pattern = "/csrf-token")
 1339    {
 21340        ArgumentException.ThrowIfNullOrWhiteSpace(pattern);
 21341        if (host.App is null)
 1342        {
 01343            throw new InvalidOperationException("WebApplication is not initialized. Call EnableConfiguration first.");
 1344        }
 11345        var options = new MapRouteOptions
 11346        {
 11347            Pattern = pattern,
 11348            HttpVerbs = [HttpVerb.Get],
 11349            ScriptCode = new LanguageOptions
 11350            {
 11351                Language = ScriptLanguage.Native
 11352            },
 11353            DisableAntiforgery = true,
 11354            AllowAnonymous = true,
 11355        };
 1356
 1357        // OpenAPI = new() { Summary = "Get CSRF token", Description = "Returns antiforgery request token and header nam
 1358
 1359        // Map directly and write directly (no KestrunResponse.ApplyTo)
 11360        var map = host.App.MapMethods(options.Pattern, [HttpMethods.Get], async context =>
 11361        {
 11362            var af = context.RequestServices.GetRequiredService<IAntiforgery>();
 11363            var opts = context.RequestServices.GetRequiredService<IOptions<AntiforgeryOptions>>();
 11364
 11365            var tokens = af.GetAndStoreTokens(context);
 11366
 11367            // Strongly discourage caches (proxies/browsers) from storing this payload
 11368            context.Response.Headers.CacheControl = "no-store, no-cache, must-revalidate";
 11369            context.Response.Headers.Pragma = "no-cache";
 11370            context.Response.Headers.Expires = "0";
 11371
 11372            context.Response.ContentType = "application/json";
 11373            await context.Response.WriteAsJsonAsync(new
 11374            {
 11375                token = tokens.RequestToken,
 11376                headerName = opts.Value.HeaderName // may be null if not configured
 11377            });
 21378        });
 1379
 1380        // Apply your pipeline metadata (this adds DisableAntiforgery, CORS, rate limiting, OpenAPI, etc.)
 11381        host.AddMapOptions(map, options);
 1382
 1383        // (Optional) track in your registry for consistency / duplicate checks
 11384        host._registeredRoutes[(options.Pattern, HttpVerb.Get)] = options;
 1385
 11386        host.Logger.Information("Added token endpoint: {Pattern} (GET)", options.Pattern);
 11387        return map;
 1388    }
 1389
 1390    private static bool IsUnsafeVerb(HttpVerb v)
 51391        => v is HttpVerb.Post or HttpVerb.Put or HttpVerb.Patch or HttpVerb.Delete;
 1392
 1393    private static bool IsUnsafeMethod(string method)
 241394        => HttpMethods.IsPost(method) || HttpMethods.IsPut(method) || HttpMethods.IsPatch(method) || HttpMethods.IsDelet
 1395
 1396    // New precise helper: only validate for the actual incoming request method when that method is unsafe and antiforge
 1397    private static bool ShouldValidateCsrf(MapRouteOptions o, HttpContext ctx)
 1398    {
 251399        if (o.DisableAntiforgery)
 1400        {
 11401            return false;
 1402        }
 241403        if (!IsUnsafeMethod(ctx.Request.Method))
 1404        {
 181405            return false; // Safe verb (GET/HEAD/OPTIONS) -> skip
 1406        }
 1407        // Ensure the route was actually configured for this unsafe verb (defensive; normally true inside mapped delegat
 191408        return o.HttpVerbs.Any(v => string.Equals(v.ToMethodString(), ctx.Request.Method, StringComparison.OrdinalIgnore
 1409    }
 1410
 1411    private static async Task<bool> TryValidateAntiforgeryAsync(HttpContext ctx)
 1412    {
 11413        var af = ctx.RequestServices.GetService<IAntiforgery>();
 11414        if (af is null)
 1415        {
 11416            return true; // antiforgery not configured → do nothing
 1417        }
 1418
 1419        try
 1420        {
 01421            await af.ValidateRequestAsync(ctx);
 01422            return true;
 1423        }
 01424        catch (AntiforgeryValidationException ex)
 1425        {
 1426            // short-circuit with RFC 9110 problem+json
 01427            ctx.Response.StatusCode = StatusCodes.Status400BadRequest;
 01428            ctx.Response.ContentType = "application/problem+json";
 01429            await ctx.Response.WriteAsJsonAsync(new
 01430            {
 01431                type = "https://datatracker.ietf.org/doc/html/rfc9110#section-15.5.1",
 01432                title = "Antiforgery validation failed",
 01433                status = 400,
 01434                detail = ex.Message
 01435            });
 01436            return false;
 1437        }
 11438    }
 1439
 1440    /// <summary>
 1441    /// Matches a bracketed IPv6 host:port specification in the format "[ipv6]:port", where:
 1442    /// - ipv6 is a valid IPv6 address (e.g. "::1", "2001:0db8:85a3:0000:0000:8a2e:0370:7334")
 1443    /// - port is a numeric value between 1 and 65535
 1444    /// Examples of valid inputs:
 1445    ///   "[::1]:80"
 1446    ///   "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:443"
 1447    /// </summary>
 1448    [GeneratedRegex(@"^\[([^\]]+)\]:(\d+)$")]
 1449    private static partial Regex BracketedIpv6SpecMatcher();
 1450
 1451    /// <summary>
 1452    /// Matches a host:port specification in the format "host:port", where:
 1453    /// - host can be any string excluding ':' (to avoid confusion with IPv6 addresses)
 1454    /// - port is a numeric value between 1 and 65535
 1455    /// Examples of valid inputs:
 1456    ///   "example.com:80"
 1457    ///   "localhost:443"
 1458    ///   "[::1]:8080"  (IPv6 address in brackets)
 1459    /// </summary>
 1460    [GeneratedRegex(@"^([^:]+):(\d+)$")]
 1461    private static partial Regex HostPortSpecMatcher();
 1462
 1463    /// <summary>
 1464    /// Matches a URL that starts with "http://" or "https://", followed by a host (excluding '/', '?', or '#'), and end
 1465    /// Examples of valid inputs:
 1466    ///   "http://example.com:"
 1467    ///   "https://localhost:"
 1468    ///   "https://my-server:8080:"
 1469    /// </summary>
 1470    [GeneratedRegex(@"^https?://[^/\?#]+:$", RegexOptions.IgnoreCase, "en-US")]
 1471    private static partial Regex EmptyPortDetectionRegex();
 1472}

Methods/Properties

AddFormRoute(Kestrun.Hosting.KestrunHost,System.String,System.Management.Automation.ScriptBlock,Kestrun.Forms.KrFormOptions,System.String[],System.String[],System.String,System.Boolean)
CreateLanguageOptions(Kestrun.Forms.KrFormOptions,System.Management.Automation.ScriptBlock)
BuildFormRouteMapOptions(System.String,System.Management.Automation.ScriptBlock,Kestrun.Forms.KrFormOptions,System.String[],System.String[],System.String,System.Boolean,System.Boolean,System.String,System.String[],System.String,System.String,System.String[])
ApplyAuthorizationOptions(Kestrun.Hosting.Options.MapRouteOptions,System.Boolean,System.String[],System.String[])
ApplyOpenApiMetadata(Kestrun.Hosting.Options.MapRouteOptions,System.String,Kestrun.Forms.KrFormOptions,System.Boolean,System.String,System.String[],System.String,System.String,System.String[])
GetFormRouteWrapperScript(System.Management.Automation.ScriptBlock)
BuildOpenApiRequestBody(Kestrun.Forms.KrFormOptions)
IsProbablyFileRule(Kestrun.Forms.KrFormPartRule)
CreateRuleSchema(System.Boolean,System.Boolean)
MergeMapRouteOptions(Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Hosting.Options.MapRouteOptions)
MergePattern(System.String,System.String)
MergeUnique(System.String[],System.String[])
AddNonEmptyValues(System.Collections.Generic.HashSet`1<System.String>,System.String[])
MergeRefs(System.Reflection.Assembly[],System.Reflection.Assembly[])
MergeArguments(System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
TryParse(System.String,System.String&,System.Int32&,System.Nullable`1<System.Boolean>&)
AddMapRoute(Kestrun.Hosting.KestrunHost,System.String,Kestrun.Utilities.HttpVerb,Kestrun.Hosting.KestrunHostMapExtensions/KestrunHandler,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder&,System.Collections.Generic.List`1<System.String>)
AddMapRoute(Kestrun.Hosting.KestrunHost,System.String,System.Collections.Generic.IEnumerable`1<Kestrun.Utilities.HttpVerb>,Kestrun.Hosting.KestrunHostMapExtensions/KestrunHandler,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder&,System.Collections.Generic.List`1<System.String>)
AddMapRoute(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Hosting.KestrunHostMapExtensions/KestrunHandler,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder&)
AddOpenApiMapRoute(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.OpenApiMapRouteOptions)
AddMapRoute(Kestrun.Hosting.KestrunHost,System.String,Kestrun.Utilities.HttpVerb,System.String,Kestrun.Scripting.ScriptLanguage,System.Collections.Generic.List`1<System.String>,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
AddMapRoute(Kestrun.Hosting.KestrunHost,System.String,System.Collections.Generic.IEnumerable`1<Kestrun.Utilities.HttpVerb>,System.String,Kestrun.Scripting.ScriptLanguage,System.Collections.Generic.List`1<System.String>,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
AddMapRoute(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions)
CreateMapRoute(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions)
ValidateRouteOptions(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions,Kestrun.Hosting.Options.MapRouteOptions&)
CompileScript(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.LanguageOptions)
handler()
CreateAndRegisterRoute(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions,Microsoft.AspNetCore.Http.RequestDelegate)
AddMapOptions(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
TryParseEndpointSpec(System.String,System.String&,System.Int32&,System.Nullable`1<System.Boolean>&)
TryParseUrlSpec(System.String,System.String&,System.Int32&,System.Nullable`1<System.Boolean>&)
TryParseBracketedIpv6Spec(System.String,System.String&,System.Int32&)
TryParseHostPortSpec(System.String,System.String&,System.Int32&)
IsValidPort(System.Int32)
ToRequireHost(System.String,System.Int32)
IsIPv6Address(System.String)
ApplyRequiredHost(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
ApplyKestrunConventions(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,System.Action`1<Kestrun.Hosting.Options.MapRouteOptions>)
AddMetadata(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
ApplyShortCircuit(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
ApplyAnonymous(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
DisableAntiforgery(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
DisableResponseCompression(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
ApplyRateLimiting(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
ApplyAuthSchemes(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
ApplyPolicies(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
ApplyCors(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.MapRouteOptions)
ApplyOpenApiMetadata(Kestrun.Hosting.KestrunHost,Microsoft.AspNetCore.Builder.IEndpointConventionBuilder,Kestrun.Hosting.Options.OpenAPIPathMetadata)
AddHtmlTemplateRoute(Kestrun.Hosting.KestrunHost,System.String,System.String,System.Collections.Generic.List`1<System.String>)
AddHtmlTemplateRoute(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions,System.String)
AddSwaggerUiRoute(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions,System.Uri)
AddRedocUiRoute(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions,System.Uri)
AddScalarUiRoute(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions,System.Uri)
AddRapiDocUiRoute(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions,System.Uri)
AddElementsUiRoute(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions,System.Uri)
AddOpenApiUiRoute(Kestrun.Hosting.KestrunHost,Kestrun.Hosting.Options.MapRouteOptions,System.Uri,System.String,System.String,System.String)
AddHtmlRouteFromEmbeddedResource(Kestrun.Hosting.KestrunHost,System.String,System.Uri,System.String)
MapExists(Kestrun.Hosting.KestrunHost,System.String,System.Collections.Generic.IEnumerable`1<Kestrun.Utilities.HttpVerb>)
MapExists(Kestrun.Hosting.KestrunHost,System.String,Kestrun.Utilities.HttpVerb)
GetMapRouteOptions(Kestrun.Hosting.KestrunHost,System.String,Kestrun.Utilities.HttpVerb)
NormalizeCatchAllPattern(System.String)
AddAntiforgeryTokenRoute(Kestrun.Hosting.KestrunHost,System.String)
IsUnsafeVerb(Kestrun.Utilities.HttpVerb)
IsUnsafeMethod(System.String)
ShouldValidateCsrf(Kestrun.Hosting.Options.MapRouteOptions,Microsoft.AspNetCore.Http.HttpContext)
TryValidateAntiforgeryAsync()