< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Hosting.KestrunHostMapExtensions
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Hosting/KestrunHostMapExtensions.cs
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
53%
Covered lines: 291
Uncovered lines: 256
Coverable lines: 547
Total lines: 1460
Line coverage: 53.1%
Branch coverage
72%
Covered branches: 173
Total branches: 239
Branch coverage: 72.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 09/08/2025 - 20:34:03 Line coverage: 54.6% (140/256) Branch coverage: 60.5% (66/109) Total lines: 722 Tag: Kestrun/Kestrun@3790ee5884494a7a2a829344a47743e0bf492e7210/13/2025 - 16:52:37 Line coverage: 62.5% (281/449) Branch coverage: 77.7% (164/211) Total lines: 1260 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e10/15/2025 - 21:27:26 Line coverage: 71.1% (286/402) Branch coverage: 81.3% (170/209) Total lines: 1149 Tag: Kestrun/Kestrun@c33ec02a85e4f8d6061aeaab5a5e8c3a8b66559410/15/2025 - 22:47:55 Line coverage: 71.1% (286/402) Branch coverage: 81.3% (170/209) Total lines: 1148 Tag: Kestrun/Kestrun@f97c41150c4de89829eca919cc8b9b7e7df3df8e11/14/2025 - 12:29:34 Line coverage: 71.1% (286/402) Branch coverage: 81.3% (170/209) Total lines: 1146 Tag: Kestrun/Kestrun@5e12b09a6838e68e704cd3dc975331b9e680a62612/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@67ed8a99376189d7ed94adba1b1854518edd75d9 09/08/2025 - 20:34:03 Line coverage: 54.6% (140/256) Branch coverage: 60.5% (66/109) Total lines: 722 Tag: Kestrun/Kestrun@3790ee5884494a7a2a829344a47743e0bf492e7210/13/2025 - 16:52:37 Line coverage: 62.5% (281/449) Branch coverage: 77.7% (164/211) Total lines: 1260 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e10/15/2025 - 21:27:26 Line coverage: 71.1% (286/402) Branch coverage: 81.3% (170/209) Total lines: 1149 Tag: Kestrun/Kestrun@c33ec02a85e4f8d6061aeaab5a5e8c3a8b66559410/15/2025 - 22:47:55 Line coverage: 71.1% (286/402) Branch coverage: 81.3% (170/209) Total lines: 1148 Tag: Kestrun/Kestrun@f97c41150c4de89829eca919cc8b9b7e7df3df8e11/14/2025 - 12:29:34 Line coverage: 71.1% (286/402) Branch coverage: 81.3% (170/209) Total lines: 1146 Tag: Kestrun/Kestrun@5e12b09a6838e68e704cd3dc975331b9e680a62612/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@67ed8a99376189d7ed94adba1b1854518edd75d9

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
TryParse(...)100%11100%
AddMapRoute(...)100%11100%
AddMapRoute(...)100%22100%
AddMapRoute(...)66.66%7668%
AddOpenApiMapRoute(...)100%210%
AddMapRoute(...)83.33%66100%
AddMapRoute(...)100%44100%
AddMapRoute(...)100%44100%
CreateMapRoute(...)75%5461.11%
ValidateRouteOptions(...)91.66%121293.75%
CompileScript(...)57.14%8770%
handler()50%5460%
CreateAndRegisterRoute(...)83.33%6691.66%
AddMapOptions(...)100%11100%
TryParseEndpointSpec(...)100%88100%
TryParseUrlSpec(...)81.25%161694.44%
TryParseBracketedIpv6Spec(...)66.66%6687.5%
TryParseHostPortSpec(...)100%66100%
IsValidPort(...)100%22100%
ToRequireHost(...)100%22100%
IsIPv6Address(...)100%22100%
ApplyRequiredHost(...)96.15%2626100%
ApplyKestrunConventions(...)100%11100%
AddMetadata(...)50%521025%
ApplyShortCircuit(...)25%10428.57%
ApplyAnonymous(...)50%2260%
DisableAntiforgery(...)50%3240%
DisableResponseCompression(...)100%22100%
ApplyRateLimiting(...)50%3240%
ApplyAuthSchemes(...)100%88100%
ApplyPolicies(...)100%88100%
ApplyCors(...)33.33%14640%
ApplyOpenApiMetadata(...)0%7280%
AddHtmlTemplateRoute(...)0%620%
AddHtmlTemplateRoute(...)88.88%221876.92%
AddSwaggerUiRoute(...)100%210%
AddRedocUiRoute(...)100%210%
AddScalarUiRoute(...)100%210%
AddRapiDocUiRoute(...)100%210%
AddElementsUiRoute(...)100%210%
AddOpenApiUiRoute(...)0%210140%
AddHtmlRouteFromEmbeddedResource(...)100%210%
MapExists(...)100%11100%
MapExists(...)100%11100%
GetMapRouteOptions(...)50%22100%
NormalizeCatchAllPattern(...)50%22100%
AddAntiforgeryTokenRoute(...)0%620%
IsUnsafeVerb(...)80%1010100%
IsUnsafeMethod(...)100%66100%
ShouldValidateCsrf(...)100%66100%
TryValidateAntiforgeryAsync()0%620%

File(s)

/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
 259    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    {
 274        return host.AddMapRoute(new MapRouteOptions
 275        {
 276            Pattern = pattern,
 277            HttpVerbs = [.. httpVerbs],
 278            ScriptCode = new LanguageOptions
 279            {
 280                Language = ScriptLanguage.Native,
 281            },
 282            RequireSchemes = requireSchemes ?? [] // No authorization by default
 283        }, 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    {
 296        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 97        {
 198            host.Logger.Debug("AddMapRoute called with options={Options}", options);
 99        }
 100        // Ensure the WebApplication is initialized
 2101        if (host.App is null)
 102        {
 0103            throw new InvalidOperationException("WebApplication is not initialized. Call EnableConfiguration first.");
 104        }
 105
 106        // Validate options
 1107        if (string.IsNullOrWhiteSpace(options.Pattern))
 108        {
 0109            throw new ArgumentException("Pattern cannot be null or empty.", nameof(options.Pattern));
 110        }
 111
 2112        string[] methods = [.. options.HttpVerbs.Select(v => v.ToMethodString())];
 1113        map = host.App.MapMethods(options.Pattern, methods, async context =>
 1114         {
 1115             // 🔒 CSRF validation only for the current request when that verb is unsafe (unless disabled)
 0116             if (ShouldValidateCsrf(options, context))
 1117             {
 0118                 if (!await TryValidateAntiforgeryAsync(context))
 1119                 {
 0120                     return; // already responded 400
 1121                 }
 1122             }
 0123             var kestrunContext = new KestrunContext(host, context);
 0124             await handler(kestrunContext);
 0125             await kestrunContext.Response.ApplyTo(context.Response);
 1126         });
 127
 1128        host.AddMapOptions(map, options);
 129
 1130        host.Logger.Information("Added native route: {Pattern} with methods: {Methods}", options.Pattern, string.Join(",
 131        // Add to the feature queue for later processing
 1132        host.FeatureQueue.Add(host => host.AddMapRoute(options));
 1133        return host;
 134    }
 135
 136    /// <summary>
 137    /// Adds a route to the KestrunHost that serves OpenAPI documents based on the provided options.
 138    /// </summary>
 139    /// <param name="host">The KestrunHost instance.</param>
 140    /// <param name="options">The OpenApiMapRouteOptions instance.</param>
 141    /// <returns>The KestrunHost instance for chaining.</returns>
 142    public static KestrunHost AddOpenApiMapRoute(this KestrunHost host, OpenApiMapRouteOptions options)
 143    {
 0144        ArgumentNullException.ThrowIfNull(options);
 0145        ArgumentNullException.ThrowIfNull(host);
 146
 147        // Validate options
 0148        return host.AddMapRoute(options.MapOptions, async context =>
 0149        {
 0150            // Extract parameters
 0151            var refresh = false;
 0152            var docId = options.DocId;
 0153            OpenApiSpecVersion specVersion;
 0154            // Try to get version and format from route values
 0155            var version = context.Request.RouteValues[options.VersionVarName]?.ToString() ?? options.DefaultVersion;
 0156            var format = context.Request.RouteValues[options.FormatVarName]?.ToString() ?? options.DefaultFormat;
 0157            if (context.Request.Query.TryGetValue(options.RefreshVarName, out var value))
 0158            {
 0159                _ = bool.TryParse(value, out refresh);
 0160            }
 0161            // Try to get version and format from route values
 0162            try
 0163            {
 0164                specVersion = OpenApiSpecVersionExtensions.ParseOpenApiSpecVersion(version);
 0165                if (format is not "json" and not "yaml")
 0166                {
 0167                    throw new InvalidOperationException($"Unsupported OpenAPI format requested: {format}");
 0168                }
 0169            }
 0170            catch
 0171            {
 0172                host.Logger.Warning("Invalid OpenAPI version or format requested: {Version}, {Format}", version, format)
 0173                context.Response.StatusCode = 404; // Not Found
 0174                return;
 0175            }
 0176            // Refresh the document if requested
 0177            if (refresh)
 0178            {
 0179                host.Logger.Information("Refreshing OpenAPI document cache as requested.");
 0180                var doc = host.OpenApiDocumentDescriptor[docId];
 0181                doc.GenerateDoc();
 0182            }
 0183            // Serve the document in the requested format
 0184            if (format == "json")
 0185            {
 0186                var json = host.OpenApiDocumentDescriptor[docId].ToJson(specVersion);
 0187                await context.Response.WriteTextResponseAsync(json, 200, "application/json");
 0188            }
 0189            else
 0190            {
 0191                var yml = host.OpenApiDocumentDescriptor[docId].ToYaml(specVersion);
 0192                await context.Response.WriteTextResponseAsync(yml, 200, "application/yaml");
 0193            }
 0194        }, out _);
 195    }
 196
 197    /// <summary>
 198    /// Adds a route to the KestrunHost that executes a script block for the specified HTTP verb and pattern.
 199    /// </summary>
 200    /// <param name="host">The KestrunHost instance.</param>
 201    /// <param name="pattern">The route pattern.</param>
 202    /// <param name="httpVerbs">The HTTP verb for the route.</param>
 203    /// <param name="scriptBlock">The script block to execute.</param>
 204    /// <param name="language">The scripting language to use (default is PowerShell).</param>
 205    /// <param name="requireSchemes">Optional array of authorization schemes required for the route.</param>
 206    /// <param name="arguments">Optional dictionary of arguments to pass to the script.</param>
 207    /// <returns>The KestrunHost instance for chaining.</returns>
 208    public static KestrunHost AddMapRoute(this KestrunHost host, string pattern, HttpVerb httpVerbs, string scriptBlock,
 209                                     List<string>? requireSchemes = null,
 210                                 Dictionary<string, object?>? arguments = null)
 211    {
 11212        arguments ??= [];
 11213        return host.AddMapRoute(new MapRouteOptions
 11214        {
 11215            Pattern = pattern,
 11216            HttpVerbs = [httpVerbs],
 11217            ScriptCode = new LanguageOptions
 11218            {
 11219                Code = scriptBlock,
 11220                Language = language,
 11221                Arguments = arguments ?? [] // No additional arguments by default
 11222            },
 11223            RequireSchemes = requireSchemes ?? [], // No authorization by default
 11224        });
 225    }
 226
 227    /// <summary>
 228    /// Adds a route to the KestrunHost that executes a script block for the specified HTTP verbs and pattern.
 229    /// </summary>
 230    /// <param name="host">The KestrunHost instance.</param>
 231    /// <param name="pattern">The route pattern.</param>
 232    /// <param name="httpVerbs">The HTTP verbs for the route.</param>
 233    /// <param name="scriptBlock">The script block to execute.</param>
 234    /// <param name="language">The scripting language to use (default is PowerShell).</param>
 235    /// <param name="requireSchemes">Optional array of authorization schemes required for the route.</param>
 236    /// <param name="arguments">Optional dictionary of arguments to pass to the script.</param>
 237    /// <returns>The KestrunHost instance for chaining.</returns>
 238    public static KestrunHost AddMapRoute(this KestrunHost host, string pattern,
 239                                IEnumerable<HttpVerb> httpVerbs,
 240                                string scriptBlock,
 241                                ScriptLanguage language = ScriptLanguage.PowerShell,
 242                                List<string>? requireSchemes = null,
 243                                 Dictionary<string, object?>? arguments = null)
 244    {
 1245        return host.AddMapRoute(new MapRouteOptions
 1246        {
 1247            Pattern = pattern,
 1248            HttpVerbs = [.. httpVerbs],
 1249            ScriptCode = new LanguageOptions
 1250            {
 1251                Code = scriptBlock,
 1252                Language = language,
 1253                Arguments = arguments ?? [] // No additional arguments by default
 1254            },
 1255            RequireSchemes = requireSchemes ?? [], // No authorization by default
 1256        });
 257    }
 258
 259    /// <summary>
 260    /// Adds a route to the KestrunHost using the specified MapRouteOptions.
 261    /// </summary>
 262    /// <param name="host">The KestrunHost instance.</param>
 263    /// <param name="options">The MapRouteOptions containing route configuration.</param>
 264    /// <returns>The KestrunHost instance for chaining.</returns>
 265    public static KestrunHost AddMapRoute(this KestrunHost host, MapRouteOptions options)
 266    {
 31267        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 268        {
 22269            host.Logger.Debug("AddMapRoute called with pattern={Pattern}, language={Language}, method={Methods}", option
 270        }
 31271        if (host.IsConfigured)
 272        {
 30273            _ = CreateMapRoute(host, options);
 274        }
 275        else
 276        {
 1277            _ = host.Use(app =>
 1278            {
 1279                _ = CreateMapRoute(host, options);
 2280            });
 281        }
 25282        return host; // for chaining
 283    }
 284
 285    /// <summary>
 286    /// Adds a route to the KestrunHost using the specified MapRouteOptions.
 287    /// </summary>
 288    /// <param name="host">The KestrunHost instance.</param>
 289    /// <param name="options">The MapRouteOptions containing route configuration.</param>
 290    /// <returns>The IEndpointConventionBuilder for the created route.</returns>
 291    private static IEndpointConventionBuilder CreateMapRoute(KestrunHost host, MapRouteOptions options)
 292    {
 31293        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 294        {
 22295            host.Logger.Debug("AddMapRoute called with pattern={Pattern}, language={Language}, method={Methods}", option
 296        }
 297
 298        try
 299        {
 300            // Validate options and get normalized route options
 31301            if (!ValidateRouteOptions(host, options, out var routeOptions))
 302            {
 0303                return null!; // Route already exists and should be skipped
 304            }
 305
 30306            var logger = host.Logger.ForContext("Route", routeOptions.Pattern);
 307
 308            // Compile the script once – return a RequestDelegate
 30309            var compiled = CompileScript(host, options.ScriptCode);
 310
 311            // Create and register the route
 30312            return CreateAndRegisterRoute(host, routeOptions, compiled);
 313        }
 0314        catch (CompilationErrorException ex)
 315        {
 316            // Log the detailed compilation errors
 0317            host.Logger.Error($"Failed to add route '{options.Pattern}' due to compilation errors:");
 0318            host.Logger.Error(ex.GetDetailedErrorMessage());
 319
 320            // Re-throw with additional context
 0321            throw new InvalidOperationException(
 0322                $"Failed to compile {options.ScriptCode.Language} script for route '{options.Pattern}'. {ex.GetErrors().
 0323                ex);
 324        }
 6325        catch (Exception ex)
 326        {
 6327            throw new InvalidOperationException(
 6328                $"Failed to add route '{options.Pattern}' with method '{string.Join(", ", options.HttpVerbs)}' using {op
 6329                ex);
 330        }
 25331    }
 332
 333    /// <summary>
 334    /// Validates the host and options for adding a map route.
 335    /// </summary>
 336    /// <param name="host">The KestrunHost instance.</param>
 337    /// <param name="options">The MapRouteOptions to validate.</param>
 338    /// <param name="routeOptions">The validated route options with defaults applied.</param>
 339    /// <returns>True if validation passes and route should be added; false if duplicate route should be skipped.</retur
 340    /// <exception cref="InvalidOperationException">Thrown when WebApplication is not initialized or route already exist
 341    /// <exception cref="ArgumentException">Thrown when required options are invalid.</exception>
 342    internal static bool ValidateRouteOptions(KestrunHost host, MapRouteOptions options, out MapRouteOptions routeOption
 343    {
 344        // Ensure the WebApplication is initialized
 40345        if (host.App is null)
 346        {
 0347            throw new InvalidOperationException("WebApplication is not initialized. Call EnableConfiguration first.");
 348        }
 349
 350        // Validate options
 39351        if (string.IsNullOrWhiteSpace(options.Pattern))
 352        {
 2353            throw new ArgumentException("Pattern cannot be null or empty.", nameof(options.Pattern));
 354        }
 355
 356        // Validate code
 37357        if (string.IsNullOrWhiteSpace(options.ScriptCode.Code))
 358        {
 2359            throw new ArgumentException("ScriptBlock cannot be null or empty.", nameof(options.ScriptCode.Code));
 360        }
 361
 35362        routeOptions = options;
 35363        if (options.HttpVerbs.Count == 0)
 364        {
 365            // If no HTTP verbs were specified, default to GET.
 2366            routeOptions.HttpVerbs = [HttpVerb.Get];
 367        }
 368
 35369        if (MapExists(host, routeOptions.Pattern, routeOptions.HttpVerbs))
 370        {
 3371            var msg = $"Route '{routeOptions.Pattern}' with method(s) {string.Join(", ", routeOptions.HttpVerbs)} alread
 3372            if (options.ThrowOnDuplicate)
 373            {
 2374                throw new InvalidOperationException(msg);
 375            }
 376
 1377            host.Logger.Warning(msg);
 1378            return false; // Skip this route
 379        }
 380
 32381        return true; // Continue with route creation
 382    }
 383
 384    /// <summary>
 385    /// Compiles the script code for the specified language.
 386    /// </summary>
 387    /// <param name="host">The KestrunHost instance.</param>
 388    /// <param name="options">The language options containing the script code and language.</param>
 389    /// <returns>A compiled RequestDelegate that can handle HTTP requests.</returns>
 390    /// <exception cref="NotSupportedException">Thrown when the script language is not supported.</exception>
 391    internal static RequestDelegate CompileScript(this KestrunHost host, LanguageOptions options)
 392    {
 37393        return options.Language switch
 37394        {
 1395            ScriptLanguage.PowerShell => PowerShellDelegateBuilder.Build(host, options.Code!, options.Arguments),
 33396            ScriptLanguage.CSharp => CSharpDelegateBuilder.Build(host, options.Code!, options.Arguments, options.ExtraIm
 2397            ScriptLanguage.VBNet => VBNetDelegateBuilder.Build(host, options.Code!, options.Arguments, options.ExtraImpo
 0398            ScriptLanguage.FSharp => FSharpDelegateBuilder.Build(host, options.Code!), // F# scripting not implemented
 0399            ScriptLanguage.Python => PyDelegateBuilder.Build(host, options.Code!),
 0400            ScriptLanguage.JavaScript => JScriptDelegateBuilder.Build(host, options.Code!),
 1401            _ => throw new NotSupportedException(options.Language.ToString())
 37402        };
 403    }
 404
 405    /// <summary>
 406    /// Creates and registers a route with the specified options and compiled handler.
 407    /// </summary>
 408    /// <param name="host">The KestrunHost instance.</param>
 409    /// <param name="routeOptions">The validated route options.</param>
 410    /// <param name="compiled">The compiled script delegate.</param>
 411    /// <returns>An IEndpointConventionBuilder for further configuration.</returns>
 412    internal static IEndpointConventionBuilder CreateAndRegisterRoute(KestrunHost host, MapRouteOptions routeOptions, Re
 413    {
 414        // Wrap with CSRF validation
 415        async Task handler(HttpContext ctx)
 416        {
 11417            if (ShouldValidateCsrf(routeOptions, ctx))
 418            {
 0419                if (!await TryValidateAntiforgeryAsync(ctx))
 420                {
 0421                    return; // already responded 400
 422                }
 423            }
 11424            await compiled(ctx);
 6425        }
 426
 32427        var mapPattern = NormalizeCatchAllPattern(routeOptions.Pattern!);
 67428        string[] methods = [.. routeOptions.HttpVerbs.Select(v => v.ToMethodString())];
 32429        var map = host.App!.MapMethods(mapPattern, methods, handler).WithLanguage(routeOptions.ScriptCode.Language);
 430
 32431        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 432        {
 23433            host.Logger.Debug("Mapped route: {Pattern} with methods: {Methods}", routeOptions.Pattern, string.Join(", ",
 434        }
 435
 32436        host.AddMapOptions(map, routeOptions);
 437
 438        // Register OpenAPI metadata for each verb
 114439        foreach (var method in routeOptions.HttpVerbs)
 440        {
 30441            if (routeOptions.OpenAPI.TryGetValue(method, out var value))
 442            {
 0443                ApplyOpenApiMetadata(host, map, value);
 444            }
 445            // Register the route to prevent duplicates
 30446            host._registeredRoutes[(mapPattern, method)] = routeOptions;
 447        }
 448
 27449        host.Logger.Information("Added route: {Pattern} with methods: {Methods}", mapPattern, string.Join(", ", methods)
 27450        return map;
 451    }
 452
 453    /// <summary>
 454    /// Adds additional mapping options to the route.
 455    /// </summary>
 456    /// <param name="host">The Kestrun host.</param>
 457    /// <param name="map">The endpoint convention builder.</param>
 458    /// <param name="options">The mapping options.</param>
 459    internal static void AddMapOptions(this KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 460    {
 35461        ApplyShortCircuit(host, map, options);
 35462        ApplyAnonymous(host, map, options);
 35463        DisableAntiforgery(host, map, options);
 35464        DisableResponseCompression(host, map, options);
 35465        ApplyRateLimiting(host, map, options);
 35466        ApplyAuthSchemes(host, map, options);
 34467        ApplyPolicies(host, map, options);
 33468        ApplyCors(host, map, options);
 33469        ApplyRequiredHost(host, map, options);
 30470        AddMetadata(host, map, options);
 30471    }
 472
 473    /// <summary>
 474    /// Tries to parse an endpoint specification string into its components: host, port, and HTTPS flag.
 475    /// </summary>
 476    /// <param name="spec">The endpoint specification string.</param>
 477    /// <param name="host">The host component.</param>
 478    /// <param name="port">The port component.</param>
 479    /// <param name="https">
 480    /// Indicates HTTPS (<c>true</c>) or HTTP (<c>false</c>) when the scheme is explicitly specified via a full URL.
 481    /// For host:port forms where no scheme information is available the value is <c>null</c>.
 482    /// </param>
 483    /// <returns>
 484    /// <c>true</c> if parsing succeeds; otherwise <c>false</c> and <paramref name="host"/> will be <c>string.Empty</c> 
 485    /// </returns>
 486    /// <remarks>
 487    /// Accepted formats (in priority order):
 488    /// <list type="bullet">
 489    /// <item><description>Full URL: <c>https://host:port</c>, <c>http://host:port</c>, IPv6 literal allowed in brackets
 490    /// <item><description>Bracketed IPv6 host &amp; port: <c>[::1]:5000</c>, <c>[2001:db8::1]:8080</c>.</description></
 491    /// <item><description>Host or IPv4 with port: <c>localhost:5000</c>, <c>127.0.0.1:8080</c>, <c>example.com:443</c>.
 492    /// </list>
 493    /// Unsupported / rejected examples: non http(s) schemes (e.g. <c>ftp://</c>), missing port in host:port form, empty
 494    /// </remarks>
 495    public static bool TryParseEndpointSpec(string spec, out string host, out int port, out bool? https)
 496    {
 171497        host = ""; port = 0; https = null;
 498
 57499        if (string.IsNullOrWhiteSpace(spec))
 500        {
 5501            return false;
 502        }
 503
 504        // 1. Try full URL form first
 52505        if (TryParseUrlSpec(spec, out host, out port, out https))
 506        {
 16507            return true;
 508        }
 509
 510        // 2. Bracketed IPv6 literal with port: [::1]:5000
 36511        if (TryParseBracketedIpv6Spec(spec, out host, out port))
 512        {
 4513            return true; // https stays null (not specified)
 514        }
 515
 516        // 3. Regular host:port (hostname, IPv4, or raw IPv6 w/out brackets not supported here)
 32517        if (TryParseHostPortSpec(spec, out host, out port))
 518        {
 12519            return true; // https stays null (not specified)
 520        }
 521
 522        // No match
 60523        host = ""; port = 0; https = null;
 20524        return false;
 525    }
 526
 527    /// <summary>
 528    /// Tries to parse a full URL endpoint specification.
 529    /// </summary>
 530    /// <param name="spec">The endpoint specification string.</param>
 531    /// <param name="host">The parsed host component.</param>
 532    /// <param name="port">The parsed port component.</param>
 533    /// <param name="https">The parsed HTTPS flag.</param>
 534    /// <returns><c>true</c> if parsing succeeded; otherwise <c>false</c>.</returns>
 535    private static bool TryParseUrlSpec(string spec, out string host, out int port, out bool? https)
 536    {
 156537        host = ""; port = 0; https = null;
 538        // Fast rejection for an explicitly empty port (e.g. "https://localhost:" or "http://[::1]:")
 539        // Uri.TryCreate will happily parse these and supply the default scheme port (80/443),
 540        // which would make us treat an intentionally empty port as a valid implicit port.
 541        // The accepted formats require either no colon at all (implicit default) OR a colon followed by digits.
 542        // Therefore pattern: scheme:// host-part : end-of-string (no digits after colon) should be rejected.
 52543        if (EmptyPortDetectionRegex().IsMatch(spec))
 544        {
 2545            return false;
 546        }
 50547        if (!Uri.TryCreate(spec, UriKind.Absolute, out var uri))
 548        {
 22549            return false;
 550        }
 28551        if (!(uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) ||
 28552              uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase)))
 553        {
 12554            return false; // Not http/https → let other parsers try
 555        }
 16556        if (uri.Authority.EndsWith(':'))
 557        {
 0558            return false; // reject empty port like https://localhost:
 559        }
 16560        host = uri.Host;
 16561        port = uri.Port;
 16562        https = uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)
 16563            ? true
 16564            : uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase)
 16565                ? false
 16566                : null;
 16567        return !string.IsNullOrWhiteSpace(host) && IsValidPort(port);
 568    }
 569
 570    /// <summary>
 571    /// Tries to parse a bracketed IPv6 endpoint specification.
 572    /// </summary>
 573    /// <param name="spec">The endpoint specification string.</param>
 574    /// <param name="host">The parsed host component.</param>
 575    /// <param name="port">The parsed port component.</param>
 576    /// <returns><c>true</c> if parsing succeeded; otherwise <c>false</c>.</returns>
 577    private static bool TryParseBracketedIpv6Spec(string spec, out string host, out int port)
 578    {
 72579        host = ""; port = 0;
 36580        var m = BracketedIpv6SpecMatcher().Match(spec);
 36581        if (!m.Success)
 582        {
 32583            return false;
 584        }
 4585        host = m.Groups[1].Value;
 4586        if (!int.TryParse(m.Groups[2].Value, out port) || !IsValidPort(port))
 587        {
 0588            host = ""; port = 0; return false;
 589        }
 4590        return !string.IsNullOrWhiteSpace(host);
 591    }
 592
 593    /// <summary>
 594    /// Tries to parse a host:port endpoint specification.
 595    /// </summary>
 596    /// <param name="spec">The endpoint specification string.</param>
 597    /// <param name="host">The parsed host component.</param>
 598    /// <param name="port">The parsed port component.</param>
 599    /// <returns><c>true</c> if parsing succeeded; otherwise <c>false</c>.</returns>
 600    private static bool TryParseHostPortSpec(string spec, out string host, out int port)
 601    {
 64602        host = ""; port = 0;
 32603        var m = HostPortSpecMatcher().Match(spec);
 32604        if (!m.Success)
 605        {
 17606            return false;
 607        }
 15608        host = m.Groups[1].Value;
 15609        if (!int.TryParse(m.Groups[2].Value, out port) || !IsValidPort(port))
 610        {
 9611            host = ""; port = 0; return false;
 612        }
 12613        return !string.IsNullOrWhiteSpace(host);
 614    }
 615    private const int MIN_PORT = 1;
 616    private const int MAX_PORT = 65535;
 617
 618    /// <summary>
 619    /// Validates that the port number is within the acceptable range (1-65535).
 620    /// </summary>
 621    /// <param name="port">The port number to validate.</param>
 622    /// <returns><c>true</c> if the port number is valid; otherwise, <c>false</c>.</returns>
 35623    private static bool IsValidPort(int port) => port is >= MIN_PORT and <= MAX_PORT;
 624
 625    /// <summary>
 626    /// Formats the host and port for use in RequireHost, adding brackets for IPv6 literals.
 627    /// </summary>
 628    /// <param name="host">The host component.</param>
 629    /// <param name="port">The port component.</param>
 630    /// <returns>The formatted host and port string.</returns>
 631    internal static string ToRequireHost(string host, int port) =>
 18632        IsIPv6Address(host) ? $"[{host}]:{port}" : $"{host}:{port}"; // IPv6 literals must be bracketed in RequireHost
 633
 634    /// <summary>
 635    /// Determines if the given host string is an IPv6 address.
 636    /// </summary>
 637    /// <param name="host">The host string to check.</param>
 638    /// <returns>True if the host is an IPv6 address; otherwise, false.</returns>
 18639    private static bool IsIPv6Address(string host) => IPAddress.TryParse(host, out var ip) && ip.AddressFamily == System
 640
 641    /// <summary>
 642    /// Applies required hosts to the route based on the specified endpoints in the options.
 643    /// </summary>
 644    /// <param name="host">The Kestrun host.</param>
 645    /// <param name="map">The endpoint convention builder.</param>
 646    /// <param name="options">The mapping options.</param>
 647    /// <exception cref="ArgumentException">Thrown when the specified endpoints are invalid.</exception>
 648    internal static void ApplyRequiredHost(this KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions option
 649    {
 33650        if (options.Endpoints is not { Length: > 0 })
 651        {
 25652            return;
 653        }
 654
 8655        var listeners = host.Options.Listeners;
 8656        var require = new List<string>();
 8657        var errs = new List<string>();
 658
 38659        foreach (var spec in options.Endpoints)
 660        {
 11661            if (!TryParseEndpointSpec(spec, out var eh, out var ep, out var eHttps))
 662            {
 2663                errs.Add($"'{spec}' must be 'host:port' or 'http(s)://host:port'.");
 2664                continue;
 665            }
 666
 667            // Is the host a numeric IP?
 9668            var isNumericHost = IPAddress.TryParse(eh, out var endpointIp);
 669
 670            // Find a compatible listener: same port, scheme (if specified), and IP match if numeric host.
 9671            var match = listeners.FirstOrDefault(l =>
 19672                l.Port == ep &&
 19673                (eHttps is null || l.UseHttps == eHttps.Value) &&
 19674                (!isNumericHost ||
 19675                 l.IPAddress.Equals(endpointIp) ||
 19676                 l.IPAddress.Equals(IPAddress.Any) ||
 19677                 l.IPAddress.Equals(IPAddress.IPv6Any)));
 678
 9679            if (match is null)
 680            {
 2681                errs.Add($"'{spec}' doesn't match any configured listener. " +
 4682                         $"Known: {string.Join(", ", listeners.Select(l => l.ToString()))}");
 2683                continue;
 684            }
 685
 7686            require.Add(ToRequireHost(eh, ep));
 687        }
 688
 8689        if (errs.Count > 0)
 690        {
 3691            throw new InvalidOperationException("Invalid Endpoints:" + Environment.NewLine + "  - " + string.Join(Enviro
 692        }
 5693        if (require.Count > 0)
 694        {
 5695            host.Logger.Verbose("Applying required hosts: {RequiredHosts} to route: {Pattern}",
 5696                string.Join(", ", require), options.Pattern);
 5697            _ = map.RequireHost([.. require]);
 698        }
 5699    }
 700
 701    /// <summary>
 702    /// Applies the same route conventions used by the AddMapRoute helpers to an arbitrary endpoint.
 703    /// </summary>
 704    /// <param name="host">The Kestrun host used for validation (auth schemes/policies).</param>
 705    /// <param name="builder">The endpoint convention builder to decorate.</param>
 706    /// <param name="configure">Delegate to configure a fresh <see cref="MapRouteOptions"/> instance. Only applicable pr
 707    /// <remarks>
 708    /// This is useful when you map endpoints manually via <c>app.MapGet</c>/<c>MapPost</c> and still want consistent be
 709    /// (auth, CORS, rate limiting, antiforgery disable, OpenAPI metadata, short-circuiting) without re-implementing log
 710    /// Validation notes:
 711    ///  - Pattern, Code are ignored if not relevant.
 712    ///  - Authentication schemes and policies are validated against the host registry.
 713    ///  - OpenAPI metadata is applied only when non-empty.
 714    /// </remarks>
 715    /// <returns>The original <paramref name="builder"/> for fluent chaining.</returns>
 716    public static IEndpointConventionBuilder ApplyKestrunConventions(this KestrunHost host, IEndpointConventionBuilder b
 717    {
 1718        ArgumentNullException.ThrowIfNull(host);
 1719        ArgumentNullException.ThrowIfNull(builder);
 1720        ArgumentNullException.ThrowIfNull(configure);
 721
 722        // Start with an empty options record (only convention-related fields will matter)
 1723        var options = new MapRouteOptions
 1724        {
 1725            Pattern = string.Empty,
 1726            HttpVerbs = [],
 1727            ScriptCode = new LanguageOptions
 1728            {
 1729                Language = ScriptLanguage.Native,
 1730                Code = string.Empty
 1731            }
 1732        };
 1733        configure(options);
 734
 735        // Reuse internal helper (kept internal to avoid accidental misuse) for actual application
 1736        host.AddMapOptions(builder, options);
 1737        return builder;
 738    }
 739    /// <summary>
 740    /// Adds metadata to the route from the script parameters.
 741    /// </summary>
 742    /// <param name="host">The Kestrun host.</param>
 743    /// <param name="map">The endpoint convention builder.</param>
 744    /// <param name="options">The mapping options.</param>
 745    private static void AddMetadata(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 746    {
 30747        if (options.ScriptCode is null || options.ScriptCode.Parameters is null || options.ScriptCode.Parameters.Count =
 748        {
 30749            return;
 750        }
 751
 0752        host.Logger.Verbose("Adding metadata to route: {Pattern}", options.Pattern);
 0753        _ = map.WithMetadata(options.ScriptCode.Parameters);
 0754        options.DefaultResponseContentType ??= host.Options.DefaultResponseMediaType;
 0755        if (!string.IsNullOrWhiteSpace(options.DefaultResponseContentType))
 756        {
 0757            _ = map.WithMetadata(new DefaultResponseContentType(options.DefaultResponseContentType));
 758        }
 0759    }
 760    /// <summary>
 761    /// Applies short-circuiting behavior to the route.
 762    /// </summary>
 763    /// <param name="host">The Kestrun host.</param>
 764    /// <param name="map">The endpoint convention builder.</param>
 765    /// <param name="options">The mapping options.</param>
 766    private static void ApplyShortCircuit(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 767    {
 35768        if (!options.ShortCircuit)
 769        {
 35770            return;
 771        }
 772
 0773        host.Logger.Verbose("Short-circuiting route: {Pattern} with status code: {StatusCode}", options.Pattern, options
 0774        if (options.ShortCircuitStatusCode is null)
 775        {
 0776            throw new ArgumentException("ShortCircuitStatusCode must be set if ShortCircuit is true.", nameof(options.Sh
 777        }
 778
 0779        _ = map.ShortCircuit(options.ShortCircuitStatusCode);
 0780    }
 781
 782    /// <summary>
 783    /// Applies anonymous access behavior to the route.
 784    /// </summary>
 785    /// <param name="host">The Kestrun host.</param>
 786    /// <param name="map">The endpoint convention builder.</param>
 787    /// <param name="options">The mapping options.</param>
 788    private static void ApplyAnonymous(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 789    {
 35790        if (options.AllowAnonymous)
 791        {
 0792            host.Logger.Verbose("Allowing anonymous access for route: {Pattern}", options.Pattern);
 0793            _ = map.AllowAnonymous();
 794        }
 795        else
 796        {
 35797            host.Logger.Debug("No anonymous access allowed for route: {Pattern}", options.Pattern);
 798        }
 35799    }
 800
 801    /// <summary>
 802    /// Disables anti-forgery behavior to the route.
 803    /// </summary>
 804    /// <param name="host">The Kestrun host.</param>
 805    /// <param name="map">The endpoint convention builder.</param>
 806    /// <param name="options">The mapping options.</param>
 807    private static void DisableAntiforgery(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 808    {
 35809        if (!options.DisableAntiforgery)
 810        {
 35811            return;
 812        }
 813
 0814        _ = map.DisableAntiforgery();
 0815        host.Logger.Verbose("CSRF protection disabled for route: {Pattern}", options.Pattern);
 0816    }
 817
 818    /// <summary>
 819    /// Disables response compression for the route.
 820    /// </summary>
 821    /// <param name="host">The Kestrun host.</param>
 822    /// <param name="map">The endpoint convention builder.</param>
 823    /// <param name="options">The mapping options.</param>
 824    private static void DisableResponseCompression(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions opt
 825    {
 35826        if (!options.DisableResponseCompression)
 827        {
 34828            return;
 829        }
 830
 1831        _ = map.DisableResponseCompression();
 1832        host.Logger.Verbose("Response compression disabled for route: {Pattern}", options.Pattern);
 1833    }
 834    /// <summary>
 835    /// Applies rate limiting behavior to the route.
 836    /// </summary>
 837    /// <param name="host">The Kestrun host.</param>
 838    /// <param name="map">The endpoint convention builder.</param>
 839    /// <param name="options">The mapping options.</param>
 840    private static void ApplyRateLimiting(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 841    {
 35842        if (string.IsNullOrWhiteSpace(options.RateLimitPolicyName))
 843        {
 35844            return;
 845        }
 846
 0847        host.Logger.Verbose("Applying rate limit policy: {RateLimitPolicyName} to route: {Pattern}", options.RateLimitPo
 0848        _ = map.RequireRateLimiting(options.RateLimitPolicyName);
 0849    }
 850
 851    /// <summary>
 852    /// Applies authentication schemes to the route.
 853    /// </summary>
 854    /// <param name="host">The Kestrun host.</param>
 855    /// <param name="map">The endpoint convention builder.</param>
 856    /// <param name="options">The mapping options.</param>
 857    private static void ApplyAuthSchemes(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 858    {
 35859        if (options.RequireSchemes is not null && options.RequireSchemes.Count != 0)
 860        {
 7861            foreach (var schema in options.RequireSchemes)
 862            {
 2863                if (!host.HasAuthScheme(schema))
 864                {
 1865                    throw new ArgumentException($"Authentication scheme '{schema}' is not registered.", nameof(options.R
 866                }
 867            }
 1868            host.Logger.Verbose("Requiring authorization for route: {Pattern} with policies: {Policies}", options.Patter
 1869            _ = map.RequireAuthorization(new AuthorizeAttribute
 1870            {
 1871                AuthenticationSchemes = string.Join(',', options.RequireSchemes)
 1872            });
 873        }
 874        else
 875        {
 33876            host.Logger.Debug("No authorization required for route: {Pattern}", options.Pattern);
 877        }
 33878    }
 879
 880    /// <summary>
 881    /// Applies authorization policies to the route.
 882    /// </summary>
 883    /// <param name="host">The Kestrun host.</param>
 884    /// <param name="map">The endpoint convention builder.</param>
 885    /// <param name="options">The mapping options.</param>
 886    private static void ApplyPolicies(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 887    {
 34888        if (options.RequirePolicies is not null && options.RequirePolicies.Count != 0)
 889        {
 7890            foreach (var policy in options.RequirePolicies)
 891            {
 2892                if (!host.HasAuthPolicy(policy))
 893                {
 1894                    throw new ArgumentException($"Authorization policy '{policy}' is not registered.", nameof(options.Re
 895                }
 896            }
 1897            _ = map.RequireAuthorization(options.RequirePolicies.ToArray());
 898        }
 899        else
 900        {
 32901            host.Logger.Debug("No authorization policies required for route: {Pattern}", options.Pattern);
 902        }
 32903    }
 904    /// <summary>
 905    /// Applies CORS behavior to the route.
 906    /// </summary>
 907    /// <param name="host">The Kestrun host.</param>
 908    /// <param name="map">The endpoint convention builder.</param>
 909    /// <param name="options">The mapping options.</param>
 910    private static void ApplyCors(KestrunHost host, IEndpointConventionBuilder map, MapRouteOptions options)
 911    {
 33912        if (!string.IsNullOrWhiteSpace(options.CorsPolicy))
 913        {
 0914            if (!host.DefinedCorsPolicyNames.Contains(options.CorsPolicy))
 915            {
 0916                throw new ArgumentException($"CORS policy '{options.CorsPolicy}' is not registered.");
 917            }
 0918            host.Logger.Verbose("Applying CORS policy: {CorsPolicy} to route: {Pattern}", options.CorsPolicy, options.Pa
 0919            _ = map.RequireCors(options.CorsPolicy);
 0920            return;
 921        }
 922        // No per-route policy requested.
 33923        if (host.CorsPolicyDefined)
 924        {
 0925            host.Logger.Verbose("No per-route CORS policy set for route: {Pattern}; default CORS policy will apply.", op
 926        }
 927        else
 928        {
 33929            host.Logger.Debug("No CORS policy configured for route: {Pattern}", options.Pattern);
 930        }
 33931    }
 932
 933    /// <summary>
 934    /// Applies OpenAPI metadata to the route.
 935    /// </summary>
 936    /// <param name="host">The Kestrun host.</param>
 937    /// <param name="map">The endpoint convention builder.</param>
 938    /// <param name="openAPI">The OpenAPI metadata.</param>
 939    private static void ApplyOpenApiMetadata(KestrunHost host, IEndpointConventionBuilder map, OpenAPIPathMetadata openA
 940    {
 0941        if (!string.IsNullOrEmpty(openAPI.OperationId))
 942        {
 0943            host.Logger.Verbose("Adding OpenAPI metadata for route: {Pattern} with OperationId: {OperationId}", openAPI.
 0944            _ = map.WithName(openAPI.OperationId);
 945        }
 946
 0947        if (!string.IsNullOrWhiteSpace(openAPI.Summary))
 948        {
 0949            host.Logger.Verbose("Adding OpenAPI summary for route: {Pattern} with Summary: {Summary}", openAPI.Pattern, 
 0950            _ = map.WithSummary(openAPI.Summary);
 951        }
 952
 0953        if (!string.IsNullOrWhiteSpace(openAPI.Description))
 954        {
 0955            host.Logger.Verbose("Adding OpenAPI description for route: {Pattern} with Description: {Description}", openA
 0956            _ = map.WithDescription(openAPI.Description);
 957        }
 958
 0959        if (openAPI.Tags.Count > 0)
 960        {
 0961            host.Logger.Verbose("Adding OpenAPI tags for route: {Pattern} with Tags: {Tags}", openAPI.Pattern, string.Jo
 0962            _ = map.WithTags([.. openAPI.Tags]);
 963        }
 0964    }
 965
 966    /// <summary>
 967    /// Adds an HTML template route to the KestrunHost for the specified pattern and HTML file path.
 968    /// </summary>
 969    /// <param name="host">The KestrunHost instance.</param>
 970    /// <param name="pattern">The route pattern.</param>
 971    /// <param name="htmlFilePath">The path to the HTML template file.</param>
 972    /// <param name="requireSchemes">Optional array of authorization schemes required for the route.</param>
 973    /// <returns>An IEndpointConventionBuilder for further configuration.</returns>
 974    public static IEndpointConventionBuilder AddHtmlTemplateRoute(this KestrunHost host, string pattern, string htmlFile
 975    {
 0976        return host.AddHtmlTemplateRoute(new MapRouteOptions
 0977        {
 0978            Pattern = pattern,
 0979            HttpVerbs = [HttpVerb.Get],
 0980            RequireSchemes = requireSchemes ?? [] // No authorization by default
 0981        }, htmlFilePath);
 982    }
 983
 984    /// <summary>
 985    /// Adds an HTML template route to the KestrunHost using the specified MapRouteOptions and HTML file path.
 986    /// </summary>
 987    /// <param name="host">The KestrunHost instance.</param>
 988    /// <param name="options">The MapRouteOptions containing route configuration.</param>
 989    /// <param name="htmlFilePath">The path to the HTML template file.</param>
 990    /// <returns>An IEndpointConventionBuilder for further configuration.</returns>
 991    public static IEndpointConventionBuilder AddHtmlTemplateRoute(this KestrunHost host, MapRouteOptions options, string
 992    {
 3993        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 994        {
 2995            host.Logger.Debug("Adding HTML template route: {Pattern}", options.Pattern);
 996        }
 997
 3998        if (options.HttpVerbs.Count != 0 &&
 3999            (options.HttpVerbs.Count > 1 || options.HttpVerbs.First() != HttpVerb.Get))
 1000        {
 11001            host.Logger.Error("HTML template routes only support GET requests. Provided HTTP verbs: {HttpVerbs}", string
 11002            throw new ArgumentException("HTML template routes only support GET requests.", nameof(options.HttpVerbs));
 1003        }
 21004        if (string.IsNullOrWhiteSpace(htmlFilePath) || !File.Exists(htmlFilePath))
 1005        {
 11006            host.Logger.Error("HTML file path is null, empty, or does not exist: {HtmlFilePath}", htmlFilePath);
 11007            throw new FileNotFoundException("HTML file not found.", htmlFilePath);
 1008        }
 1009
 11010        if (string.IsNullOrWhiteSpace(options.Pattern))
 1011        {
 01012            host.Logger.Error("Pattern cannot be null or empty.");
 01013            throw new ArgumentException("Pattern cannot be null or empty.", nameof(options.Pattern));
 1014        }
 1015
 11016        _ = host.AddMapRoute(options.Pattern, HttpVerb.Get, async (ctx) =>
 11017          {
 11018              // ② Build your variables map
 01019              var vars = new Dictionary<string, object?>();
 01020              _ = VariablesMap.GetVariablesMap(ctx, ref vars);
 11021
 01022              await ctx.Response.WriteHtmlResponseFromFileAsync(htmlFilePath, vars, ctx.Response.StatusCode);
 11023          }, out var map);
 11024        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 1025        {
 11026            host.Logger.Debug("Mapped HTML template route: {Pattern} to file: {HtmlFilePath}", options.Pattern, htmlFile
 1027        }
 11028        if (map is null)
 1029        {
 01030            throw new InvalidOperationException("Failed to create HTML template route.");
 1031        }
 11032        AddMapOptions(host, map, options);
 11033        return map;
 1034    }
 1035
 1036    /// <summary>
 1037    /// Adds a Swagger UI route to the KestrunHost for the specified pattern and OpenAPI endpoint.
 1038    /// </summary>
 1039    /// <param name="host">The KestrunHost instance.</param>
 1040    /// <param name="options">The MapRouteOptions containing route configuration.</param>
 1041    /// <param name="openApiEndpoint">The URI of the OpenAPI endpoint.</param>
 1042    /// <returns>An IEndpointConventionBuilder for further configuration.</returns>
 1043    /// <exception cref="ArgumentException">Thrown when the provided options are invalid.</exception>
 1044    /// <exception cref="InvalidOperationException">Thrown when the Swagger UI route cannot be created.</exception>
 1045    public static IEndpointConventionBuilder AddSwaggerUiRoute(
 1046        this KestrunHost host,
 1047        MapRouteOptions options,
 1048        Uri openApiEndpoint)
 1049    {
 01050        return AddOpenApiUiRoute(
 01051            host,
 01052            options,
 01053            openApiEndpoint,
 01054            uiName: "Swagger",
 01055            defaultPattern: "/docs/swagger",
 01056            resourceName: "Kestrun.Assets.swagger-ui.html");
 1057    }
 1058
 1059    /// <summary>
 1060    /// Adds a Redoc UI route to the KestrunHost for the specified pattern and OpenAPI endpoint.
 1061    /// </summary>
 1062    /// <param name="host">The KestrunHost instance.</param>
 1063    /// <param name="options">The route mapping options.</param>
 1064    /// <param name="openApiEndpoint">The OpenAPI endpoint URI.</param>
 1065    /// <returns>An IEndpointConventionBuilder for the mapped route.</returns>
 1066    /// <exception cref="ArgumentException">Thrown when the provided options are invalid.</exception>
 1067    /// <exception cref="InvalidOperationException">Thrown when the Redoc UI route cannot be created.</exception>
 1068    public static IEndpointConventionBuilder AddRedocUiRoute(
 1069        this KestrunHost host,
 1070        MapRouteOptions options,
 1071        Uri openApiEndpoint)
 1072    {
 01073        return AddOpenApiUiRoute(
 01074            host,
 01075            options,
 01076            openApiEndpoint,
 01077            uiName: "Redoc",
 01078            defaultPattern: "/docs/redoc",
 01079            resourceName: "Kestrun.Assets.redoc-ui.html");
 1080    }
 1081
 1082    /// <summary>
 1083    /// Adds a Scalar UI route to the KestrunHost for the specified pattern and OpenAPI endpoint.
 1084    /// </summary>
 1085    /// <param name="host">The KestrunHost instance.</param>
 1086    /// <param name="options">The route mapping options.</param>
 1087    /// <param name="openApiEndpoint">The OpenAPI endpoint URI.</param>
 1088    /// <returns>An IEndpointConventionBuilder for the mapped route.</returns>
 1089    /// <exception cref="ArgumentException">Thrown when the provided options are invalid.</exception>
 1090    /// <exception cref="InvalidOperationException">Thrown when the Scalar UI route cannot be created.</exception>
 1091    public static IEndpointConventionBuilder AddScalarUiRoute(
 1092        this KestrunHost host,
 1093        MapRouteOptions options,
 1094        Uri openApiEndpoint)
 1095    {
 01096        return AddOpenApiUiRoute(
 01097            host,
 01098            options,
 01099            openApiEndpoint,
 01100            uiName: "Scalar",
 01101            defaultPattern: "/docs/scalar",
 01102            resourceName: "Kestrun.Assets.scalar.html");
 1103    }
 1104
 1105    /// <summary>
 1106    /// Adds a RapiDoc UI route to the KestrunHost for the specified pattern and OpenAPI endpoint.
 1107    /// </summary>
 1108    /// <param name="host">The KestrunHost instance.</param>
 1109    /// <param name="options">The route mapping options.</param>
 1110    /// <param name="openApiEndpoint">The OpenAPI endpoint URI.</param>
 1111    /// <returns>An IEndpointConventionBuilder for the mapped route.</returns>
 1112    public static IEndpointConventionBuilder AddRapiDocUiRoute(
 1113       this KestrunHost host,
 1114       MapRouteOptions options,
 1115       Uri openApiEndpoint)
 1116    {
 01117        return AddOpenApiUiRoute(
 01118            host,
 01119            options,
 01120            openApiEndpoint,
 01121            uiName: "RapiDoc",
 01122            defaultPattern: "/docs/rapidoc",
 01123            resourceName: "Kestrun.Assets.rapidoc.html");
 1124    }
 1125
 1126    /// <summary>
 1127    /// Adds an Elements UI route to the KestrunHost for the specified pattern and OpenAPI endpoint.
 1128    /// </summary>
 1129    /// <param name="host">The KestrunHost instance.</param>
 1130    /// <param name="options">The route mapping options.</param>
 1131    /// <param name="openApiEndpoint">The OpenAPI endpoint URI.</param>
 1132    /// <returns>An IEndpointConventionBuilder for the mapped route.</returns>
 1133    public static IEndpointConventionBuilder AddElementsUiRoute(
 1134      this KestrunHost host,
 1135      MapRouteOptions options,
 1136      Uri openApiEndpoint)
 1137    {
 01138        return AddOpenApiUiRoute(
 01139            host,
 01140            options,
 01141            openApiEndpoint,
 01142            uiName: "Elements",
 01143            defaultPattern: "/docs/elements",
 01144            resourceName: "Kestrun.Assets.elements.html");
 1145    }
 1146
 1147    /// <summary>
 1148    /// Adds an OpenAPI UI route to the KestrunHost for the specified pattern and OpenAPI endpoint.
 1149    /// </summary>
 1150    /// <param name="host">The KestrunHost instance.</param>
 1151    /// <param name="options">The route mapping options.</param>
 1152    /// <param name="openApiEndpoint">The OpenAPI endpoint URI.</param>
 1153    /// <param name="uiName">The name of the UI.</param>
 1154    /// <param name="defaultPattern">The default route pattern.</param>
 1155    /// <param name="resourceName">The embedded resource name.</param>
 1156    /// <returns>The endpoint convention builder for the mapped route.</returns>
 1157    /// <exception cref="ArgumentException">Thrown when the provided options are invalid.</exception>
 1158    /// <exception cref="InvalidOperationException">Thrown when the OpenAPI UI route cannot be created.</exception>
 1159    private static IEndpointConventionBuilder AddOpenApiUiRoute(
 1160        KestrunHost host,
 1161        MapRouteOptions options,
 1162        Uri openApiEndpoint,
 1163        string uiName,
 1164        string defaultPattern,
 1165        string resourceName)
 1166    {
 01167        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 1168        {
 01169            host.Logger.Debug(
 01170                "Adding {UiName} UI route: {Pattern} for OpenAPI endpoint: {OpenApiEndpoint}",
 01171                uiName,
 01172                options.Pattern,
 01173                openApiEndpoint);
 1174        }
 1175
 01176        if (options.HttpVerbs.Count != 0 &&
 01177            (options.HttpVerbs.Count > 1 || options.HttpVerbs.First() != HttpVerb.Get))
 1178        {
 01179            host.Logger.Error(
 01180                "{UiName} UI routes only support GET requests. Provided HTTP verbs: {HttpVerbs}",
 01181                uiName,
 01182                string.Join(", ", options.HttpVerbs));
 1183
 01184            throw new ArgumentException(
 01185                $"{uiName} UI routes only support GET requests.",
 01186                nameof(options.HttpVerbs));
 1187        }
 1188
 1189        // Set default pattern if not provided
 01190        if (string.IsNullOrWhiteSpace(options.Pattern))
 1191        {
 01192            options.Pattern = defaultPattern;
 1193        }
 1194
 1195        // Load embedded UI HTML
 01196        var map = AddHtmlRouteFromEmbeddedResource(host, options.Pattern, openApiEndpoint, resourceName);
 1197
 01198        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 1199        {
 01200            host.Logger.Debug(
 01201                "Mapped {UiName} UI route: {Pattern} for OpenAPI endpoint: {OpenApiEndpoint}",
 01202                uiName,
 01203                options.Pattern,
 01204                openApiEndpoint);
 1205        }
 1206
 01207        if (map is null)
 1208        {
 01209            throw new InvalidOperationException($"Failed to create {uiName} UI route.");
 1210        }
 1211
 01212        AddMapOptions(host, map, options);
 01213        return map;
 1214    }
 1215
 1216    /// <summary>
 1217    /// Add a HTML route from an embedded resource.
 1218    /// </summary>
 1219    /// <param name="host">The KestrunHost instance.</param>
 1220    /// <param name="pattern">The route pattern.</param>
 1221    /// <param name="openApiEndpoint">The OpenAPI endpoint URI.</param>
 1222    /// <param name="embeddedResource">The embedded resource name.</param>
 1223    /// <exception cref="InvalidOperationException"></exception>
 1224    private static IEndpointConventionBuilder? AddHtmlRouteFromEmbeddedResource(KestrunHost host, string pattern, Uri op
 1225    {
 01226        _ = host.AddMapRoute(pattern: pattern, httpVerb: HttpVerb.Get, async (ctx) =>
 01227          {
 01228              var asm = typeof(KestrunHostMapExtensions).Assembly;
 01229              using var stream = asm.GetManifestResourceStream(embeddedResource)
 01230                    ?? throw new InvalidOperationException($"Embedded HTML resource not found: {embeddedResource}");
 01231
 01232              using var ms = new MemoryStream();
 01233              stream.CopyTo(ms);
 01234              var htmlBuffer = ms.ToArray();
 01235              ctx.Response.ContentType = "text/html; charset=utf-8";
 01236              await ctx.Response.WriteHtmlResponseAsync(htmlBuffer, new Dictionary<string, object?>
 01237              {
 01238                  { "OPENAPI_ENDPOINT", openApiEndpoint.ToString() }
 01239              }, ctx.Response.StatusCode);
 01240          }, out var map);
 01241        return map;
 1242    }
 1243
 1244    /// <summary>
 1245    /// Checks if a route with the specified pattern and optional HTTP method exists in the KestrunHost.
 1246    /// </summary>
 1247    /// <param name="host">The KestrunHost instance.</param>
 1248    /// <param name="pattern">The route pattern to check.</param>
 1249    /// <param name="verbs">The optional HTTP method to check for the route.</param>
 1250    /// <returns>True if the route exists; otherwise, false.</returns>
 1251    public static bool MapExists(this KestrunHost host, string pattern, IEnumerable<HttpVerb> verbs)
 1252    {
 391253        var normalizedPattern = NormalizeCatchAllPattern(pattern);
 801254        var methodSet = verbs.Select(v => v.ToMethodString()).ToHashSet(StringComparer.OrdinalIgnoreCase);
 391255        return host._registeredRoutes.Keys
 111256            .Where(k => string.Equals(k.Pattern, normalizedPattern, StringComparison.OrdinalIgnoreCase))
 481257            .Any(k => methodSet.Contains(k.Method.ToMethodString()));
 1258    }
 1259
 1260    /// <summary>
 1261    /// Checks if a route with the specified pattern and optional HTTP method exists in the KestrunHost.
 1262    /// </summary>
 1263    /// <param name="host">The KestrunHost instance.</param>
 1264    /// <param name="pattern">The route pattern to check.</param>
 1265    /// <param name="verb">The optional HTTP method to check for the route.</param>
 1266    /// <returns>True if the route exists; otherwise, false.</returns>
 1267    public static bool MapExists(this KestrunHost host, string pattern, HttpVerb verb)
 1268    {
 91269        var normalizedPattern = NormalizeCatchAllPattern(pattern);
 91270        return host._registeredRoutes.ContainsKey((normalizedPattern, verb));
 1271    }
 1272
 1273    /// <summary>
 1274    /// Retrieves the <see cref="MapRouteOptions"/> associated with a given route pattern and HTTP verb, if registered.
 1275    /// </summary>
 1276    /// <param name="host">The <see cref="KestrunHost"/> instance to search for registered routes.</param>
 1277    /// <param name="pattern">The route pattern to look up (e.g. <c>"/hello"</c>).</param>
 1278    /// <param name="verb">The HTTP verb to match (e.g. <see cref="HttpVerb.Get"/>).</param>
 1279    /// <returns>
 1280    /// The <see cref="MapRouteOptions"/> instance for the specified route if found; otherwise, <c>null</c>.
 1281    /// </returns>
 1282    /// <remarks>
 1283    /// This method checks the internal route registry and returns the route options if the pattern and verb
 1284    /// combination was previously added via <c>AddMapRoute</c>.
 1285    /// This lookup is case-insensitive for both the pattern and method.
 1286    /// </remarks>
 1287    /// <example>
 1288    /// <code>
 1289    /// var options = host.GetMapRouteOptions("/hello", HttpVerb.Get);
 1290    /// if (options != null)
 1291    /// {
 1292    ///     Console.WriteLine($"Route language: {options.Language}");
 1293    /// }
 1294    /// </code>
 1295    /// </example>
 1296    public static MapRouteOptions? GetMapRouteOptions(this KestrunHost host, string pattern, HttpVerb verb)
 1297    {
 41298        var normalizedPattern = NormalizeCatchAllPattern(pattern);
 41299        return host._registeredRoutes.TryGetValue((normalizedPattern, verb), out var options)
 41300            ? options
 41301            : null;
 1302    }
 1303
 1304    /// <summary>
 1305    /// Normalizes catch-all parameters that use "{**name}" into "{*name}" for ASP.NET Core routing.
 1306    /// </summary>
 1307    /// <param name="pattern">The route pattern to normalize.</param>
 1308    /// <returns>The normalized route pattern.</returns>
 1309    private static string NormalizeCatchAllPattern(string pattern)
 1310    {
 841311        return string.IsNullOrWhiteSpace(pattern)
 841312            ? pattern
 841313            : pattern.Replace("{**", "{*", StringComparison.Ordinal);
 1314    }
 1315
 1316    /// <summary>
 1317    /// Adds a GET endpoint that issues the antiforgery cookie and returns a JSON payload:
 1318    /// { token: "...", headerName: "X-CSRF-TOKEN" }.
 1319    /// The endpoint itself is exempt from antiforgery validation.
 1320    /// </summary>
 1321    /// <param name="host">The KestrunHost instance.</param>
 1322    /// <param name="pattern">The route path to expose (default "/csrf-token").</param>
 1323    /// <returns>IEndpointConventionBuilder for further configuration.</returns>
 1324    public static IEndpointConventionBuilder AddAntiforgeryTokenRoute(
 1325    this KestrunHost host,
 1326    string pattern = "/csrf-token")
 1327    {
 01328        ArgumentException.ThrowIfNullOrWhiteSpace(pattern);
 01329        if (host.App is null)
 1330        {
 01331            throw new InvalidOperationException("WebApplication is not initialized. Call EnableConfiguration first.");
 1332        }
 01333        var options = new MapRouteOptions
 01334        {
 01335            Pattern = pattern,
 01336            HttpVerbs = [HttpVerb.Get],
 01337            ScriptCode = new LanguageOptions
 01338            {
 01339                Language = ScriptLanguage.Native
 01340            },
 01341            DisableAntiforgery = true,
 01342            AllowAnonymous = true,
 01343        };
 1344
 1345        // OpenAPI = new() { Summary = "Get CSRF token", Description = "Returns antiforgery request token and header nam
 1346
 1347        // Map directly and write directly (no KestrunResponse.ApplyTo)
 01348        var map = host.App.MapMethods(options.Pattern, [HttpMethods.Get], async context =>
 01349        {
 01350            var af = context.RequestServices.GetRequiredService<IAntiforgery>();
 01351            var opts = context.RequestServices.GetRequiredService<IOptions<AntiforgeryOptions>>();
 01352
 01353            var tokens = af.GetAndStoreTokens(context);
 01354
 01355            // Strongly discourage caches (proxies/browsers) from storing this payload
 01356            context.Response.Headers.CacheControl = "no-store, no-cache, must-revalidate";
 01357            context.Response.Headers.Pragma = "no-cache";
 01358            context.Response.Headers.Expires = "0";
 01359
 01360            context.Response.ContentType = "application/json";
 01361            await context.Response.WriteAsJsonAsync(new
 01362            {
 01363                token = tokens.RequestToken,
 01364                headerName = opts.Value.HeaderName // may be null if not configured
 01365            });
 01366        });
 1367
 1368        // Apply your pipeline metadata (this adds DisableAntiforgery, CORS, rate limiting, OpenAPI, etc.)
 01369        host.AddMapOptions(map, options);
 1370
 1371        // (Optional) track in your registry for consistency / duplicate checks
 01372        host._registeredRoutes[(options.Pattern, HttpVerb.Get)] = options;
 1373
 01374        host.Logger.Information("Added token endpoint: {Pattern} (GET)", options.Pattern);
 01375        return map;
 1376    }
 1377
 1378    private static bool IsUnsafeVerb(HttpVerb v)
 41379        => v is HttpVerb.Post or HttpVerb.Put or HttpVerb.Patch or HttpVerb.Delete;
 1380
 1381    private static bool IsUnsafeMethod(string method)
 191382        => HttpMethods.IsPost(method) || HttpMethods.IsPut(method) || HttpMethods.IsPatch(method) || HttpMethods.IsDelet
 1383
 1384    // New precise helper: only validate for the actual incoming request method when that method is unsafe and antiforge
 1385    private static bool ShouldValidateCsrf(MapRouteOptions o, HttpContext ctx)
 1386    {
 201387        if (o.DisableAntiforgery)
 1388        {
 11389            return false;
 1390        }
 191391        if (!IsUnsafeMethod(ctx.Request.Method))
 1392        {
 141393            return false; // Safe verb (GET/HEAD/OPTIONS) -> skip
 1394        }
 1395        // Ensure the route was actually configured for this unsafe verb (defensive; normally true inside mapped delegat
 171396        return o.HttpVerbs.Any(v => string.Equals(v.ToMethodString(), ctx.Request.Method, StringComparison.OrdinalIgnore
 1397    }
 1398
 1399    private static async Task<bool> TryValidateAntiforgeryAsync(HttpContext ctx)
 1400    {
 01401        var af = ctx.RequestServices.GetService<IAntiforgery>();
 01402        if (af is null)
 1403        {
 01404            return true; // antiforgery not configured → do nothing
 1405        }
 1406
 1407        try
 1408        {
 01409            await af.ValidateRequestAsync(ctx);
 01410            return true;
 1411        }
 01412        catch (AntiforgeryValidationException ex)
 1413        {
 1414            // short-circuit with RFC 9110 problem+json
 01415            ctx.Response.StatusCode = StatusCodes.Status400BadRequest;
 01416            ctx.Response.ContentType = "application/problem+json";
 01417            await ctx.Response.WriteAsJsonAsync(new
 01418            {
 01419                type = "https://datatracker.ietf.org/doc/html/rfc9110#section-15.5.1",
 01420                title = "Antiforgery validation failed",
 01421                status = 400,
 01422                detail = ex.Message
 01423            });
 01424            return false;
 1425        }
 01426    }
 1427
 1428    /// <summary>
 1429    /// Matches a bracketed IPv6 host:port specification in the format "[ipv6]:port", where:
 1430    /// - ipv6 is a valid IPv6 address (e.g. "::1", "2001:0db8:85a3:0000:0000:8a2e:0370:7334")
 1431    /// - port is a numeric value between 1 and 65535
 1432    /// Examples of valid inputs:
 1433    ///   "[::1]:80"
 1434    ///   "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:443"
 1435    /// </summary>
 1436    [GeneratedRegex(@"^\[([^\]]+)\]:(\d+)$")]
 1437    private static partial Regex BracketedIpv6SpecMatcher();
 1438
 1439    /// <summary>
 1440    /// Matches a host:port specification in the format "host:port", where:
 1441    /// - host can be any string excluding ':' (to avoid confusion with IPv6 addresses)
 1442    /// - port is a numeric value between 1 and 65535
 1443    /// Examples of valid inputs:
 1444    ///   "example.com:80"
 1445    ///   "localhost:443"
 1446    ///   "[::1]:8080"  (IPv6 address in brackets)
 1447    /// </summary>
 1448    [GeneratedRegex(@"^([^:]+):(\d+)$")]
 1449    private static partial Regex HostPortSpecMatcher();
 1450
 1451    /// <summary>
 1452    /// Matches a URL that starts with "http://" or "https://", followed by a host (excluding '/', '?', or '#'), and end
 1453    /// Examples of valid inputs:
 1454    ///   "http://example.com:"
 1455    ///   "https://localhost:"
 1456    ///   "https://my-server:8080:"
 1457    /// </summary>
 1458    [GeneratedRegex(@"^https?://[^/\?#]+:$", RegexOptions.IgnoreCase, "en-US")]
 1459    private static partial Regex EmptyPortDetectionRegex();
 1460}

Methods/Properties

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()