< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Hosting.KestrunHost
Assembly: Kestrun
File(s): File 1: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Hosting/KestrunHost.cs
File 2: /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Hosting/KestrunHostRazorExtensions.cs
Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff
Line coverage
69%
Covered lines: 538
Uncovered lines: 235
Coverable lines: 773
Total lines: 2177
Line coverage: 69.5%
Branch coverage
59%
Covered branches: 201
Total branches: 340
Branch coverage: 59.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 08/26/2025 - 14:53:17 Line coverage: 81.1% (232/286) Branch coverage: 71.5% (103/144) Total lines: 840 Tag: Kestrun/Kestrun@78d1e497d8ba989d121b57aa39aa3c6b22de743109/01/2025 - 04:08:24 Line coverage: 81.1% (233/287) Branch coverage: 71.5% (103/144) Total lines: 843 Tag: Kestrun/Kestrun@d6f26a131219b7a7fcb4e129af3193ec2ec4892909/04/2025 - 17:02:01 Line coverage: 81.5% (235/288) Branch coverage: 71.5% (103/144) Total lines: 844 Tag: Kestrun/Kestrun@f3880b25ea131298aa2f8b1e0d0a8d55eb160bc009/06/2025 - 18:30:33 Line coverage: 81.9% (236/288) Branch coverage: 72.2% (104/144) Total lines: 844 Tag: Kestrun/Kestrun@aeddbedb8a96e9137aac94c2d5edd011b57ac87109/07/2025 - 18:41:40 Line coverage: 79.5% (237/298) Branch coverage: 72.2% (107/148) Total lines: 861 Tag: Kestrun/Kestrun@2192d4ccb46312ce89b7f7fda1aa8c915bfa228409/08/2025 - 16:52:37 Line coverage: 74.7% (249/333) Branch coverage: 71% (108/152) Total lines: 906 Tag: Kestrun/Kestrun@14183915904bc10657c407aa6953ae9be314429f09/08/2025 - 20:34:03 Line coverage: 74% (257/347) Branch coverage: 71% (108/152) Total lines: 920 Tag: Kestrun/Kestrun@3790ee5884494a7a2a829344a47743e0bf492e7209/09/2025 - 05:44:24 Line coverage: 73.9% (255/345) Branch coverage: 71% (108/152) Total lines: 918 Tag: Kestrun/Kestrun@a26a91936c400a7f2324671b2222643fb772438109/09/2025 - 21:56:59 Line coverage: 73.9% (256/346) Branch coverage: 71% (108/152) Total lines: 919 Tag: Kestrun/Kestrun@739093f321f10605cc4d1029da7300e3bb4dcba909/12/2025 - 03:43:11 Line coverage: 73.8% (257/348) Branch coverage: 71% (108/152) Total lines: 938 Tag: Kestrun/Kestrun@d160286e3020330b1eb862d66a37db2e26fc904209/12/2025 - 16:20:13 Line coverage: 73.8% (257/348) Branch coverage: 71% (108/152) Total lines: 926 Tag: Kestrun/Kestrun@bd014be0a15f3c9298922d2ff67068869adda2a009/13/2025 - 21:11:10 Line coverage: 68.3% (257/376) Branch coverage: 60.6% (108/178) Total lines: 1032 Tag: Kestrun/Kestrun@c00c04d65edffc6840698a5c67a70cae1ad411d909/14/2025 - 21:23:16 Line coverage: 68.1% (257/377) Branch coverage: 60.6% (108/178) Total lines: 1023 Tag: Kestrun/Kestrun@c9d2f0b3dd164d7dc0dc2407a9f006293d92422309/16/2025 - 04:01:29 Line coverage: 68% (256/376) Branch coverage: 60.6% (108/178) Total lines: 1028 Tag: Kestrun/Kestrun@e5263347b0baba68d9fd62ffbf60a7dd87f994bb10/13/2025 - 16:52:37 Line coverage: 69.4% (329/474) Branch coverage: 62.1% (138/222) Total lines: 1409 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e10/15/2025 - 01:01:18 Line coverage: 65.8% (329/500) Branch coverage: 58.4% (138/236) Total lines: 1449 Tag: Kestrun/Kestrun@7c4ce528870211ad6c2d2398c31ec13097fc584010/15/2025 - 21:27:26 Line coverage: 63.6% (335/526) Branch coverage: 56% (138/246) Total lines: 1493 Tag: Kestrun/Kestrun@c33ec02a85e4f8d6061aeaab5a5e8c3a8b66559410/17/2025 - 15:48:30 Line coverage: 64.2% (354/551) Branch coverage: 56.9% (148/260) Total lines: 1571 Tag: Kestrun/Kestrun@b8199aff869a847b75e185d0527ba45e04a43d8611/14/2025 - 12:29:34 Line coverage: 63.4% (350/552) Branch coverage: 56% (148/264) Total lines: 1597 Tag: Kestrun/Kestrun@5e12b09a6838e68e704cd3dc975331b9e680a62611/19/2025 - 02:25:56 Line coverage: 63.4% (351/553) Branch coverage: 56% (148/264) Total lines: 1598 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/12/2025 - 17:27:19 Line coverage: 53.1% (316/595) Branch coverage: 43.9% (123/280) Total lines: 1690 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/14/2025 - 20:04:52 Line coverage: 52.8% (327/619) Branch coverage: 44.6% (133/298) Total lines: 1745 Tag: Kestrun/Kestrun@a05ac8de57c6207e227b92ba360e9d58869ac80a12/15/2025 - 02:23:46 Line coverage: 66% (409/619) Branch coverage: 57.3% (171/298) Total lines: 1745 Tag: Kestrun/Kestrun@7a3839f4de2254e22daae81ab8dc7cb2f40c833012/18/2025 - 21:41:58 Line coverage: 69.5% (538/773) Branch coverage: 59.1% (201/340) Total lines: 2177 Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff 08/26/2025 - 14:53:17 Line coverage: 81.1% (232/286) Branch coverage: 71.5% (103/144) Total lines: 840 Tag: Kestrun/Kestrun@78d1e497d8ba989d121b57aa39aa3c6b22de743109/01/2025 - 04:08:24 Line coverage: 81.1% (233/287) Branch coverage: 71.5% (103/144) Total lines: 843 Tag: Kestrun/Kestrun@d6f26a131219b7a7fcb4e129af3193ec2ec4892909/04/2025 - 17:02:01 Line coverage: 81.5% (235/288) Branch coverage: 71.5% (103/144) Total lines: 844 Tag: Kestrun/Kestrun@f3880b25ea131298aa2f8b1e0d0a8d55eb160bc009/06/2025 - 18:30:33 Line coverage: 81.9% (236/288) Branch coverage: 72.2% (104/144) Total lines: 844 Tag: Kestrun/Kestrun@aeddbedb8a96e9137aac94c2d5edd011b57ac87109/07/2025 - 18:41:40 Line coverage: 79.5% (237/298) Branch coverage: 72.2% (107/148) Total lines: 861 Tag: Kestrun/Kestrun@2192d4ccb46312ce89b7f7fda1aa8c915bfa228409/08/2025 - 16:52:37 Line coverage: 74.7% (249/333) Branch coverage: 71% (108/152) Total lines: 906 Tag: Kestrun/Kestrun@14183915904bc10657c407aa6953ae9be314429f09/08/2025 - 20:34:03 Line coverage: 74% (257/347) Branch coverage: 71% (108/152) Total lines: 920 Tag: Kestrun/Kestrun@3790ee5884494a7a2a829344a47743e0bf492e7209/09/2025 - 05:44:24 Line coverage: 73.9% (255/345) Branch coverage: 71% (108/152) Total lines: 918 Tag: Kestrun/Kestrun@a26a91936c400a7f2324671b2222643fb772438109/09/2025 - 21:56:59 Line coverage: 73.9% (256/346) Branch coverage: 71% (108/152) Total lines: 919 Tag: Kestrun/Kestrun@739093f321f10605cc4d1029da7300e3bb4dcba909/12/2025 - 03:43:11 Line coverage: 73.8% (257/348) Branch coverage: 71% (108/152) Total lines: 938 Tag: Kestrun/Kestrun@d160286e3020330b1eb862d66a37db2e26fc904209/12/2025 - 16:20:13 Line coverage: 73.8% (257/348) Branch coverage: 71% (108/152) Total lines: 926 Tag: Kestrun/Kestrun@bd014be0a15f3c9298922d2ff67068869adda2a009/13/2025 - 21:11:10 Line coverage: 68.3% (257/376) Branch coverage: 60.6% (108/178) Total lines: 1032 Tag: Kestrun/Kestrun@c00c04d65edffc6840698a5c67a70cae1ad411d909/14/2025 - 21:23:16 Line coverage: 68.1% (257/377) Branch coverage: 60.6% (108/178) Total lines: 1023 Tag: Kestrun/Kestrun@c9d2f0b3dd164d7dc0dc2407a9f006293d92422309/16/2025 - 04:01:29 Line coverage: 68% (256/376) Branch coverage: 60.6% (108/178) Total lines: 1028 Tag: Kestrun/Kestrun@e5263347b0baba68d9fd62ffbf60a7dd87f994bb10/13/2025 - 16:52:37 Line coverage: 69.4% (329/474) Branch coverage: 62.1% (138/222) Total lines: 1409 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e10/15/2025 - 01:01:18 Line coverage: 65.8% (329/500) Branch coverage: 58.4% (138/236) Total lines: 1449 Tag: Kestrun/Kestrun@7c4ce528870211ad6c2d2398c31ec13097fc584010/15/2025 - 21:27:26 Line coverage: 63.6% (335/526) Branch coverage: 56% (138/246) Total lines: 1493 Tag: Kestrun/Kestrun@c33ec02a85e4f8d6061aeaab5a5e8c3a8b66559410/17/2025 - 15:48:30 Line coverage: 64.2% (354/551) Branch coverage: 56.9% (148/260) Total lines: 1571 Tag: Kestrun/Kestrun@b8199aff869a847b75e185d0527ba45e04a43d8611/14/2025 - 12:29:34 Line coverage: 63.4% (350/552) Branch coverage: 56% (148/264) Total lines: 1597 Tag: Kestrun/Kestrun@5e12b09a6838e68e704cd3dc975331b9e680a62611/19/2025 - 02:25:56 Line coverage: 63.4% (351/553) Branch coverage: 56% (148/264) Total lines: 1598 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/12/2025 - 17:27:19 Line coverage: 53.1% (316/595) Branch coverage: 43.9% (123/280) Total lines: 1690 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/14/2025 - 20:04:52 Line coverage: 52.8% (327/619) Branch coverage: 44.6% (133/298) Total lines: 1745 Tag: Kestrun/Kestrun@a05ac8de57c6207e227b92ba360e9d58869ac80a12/15/2025 - 02:23:46 Line coverage: 66% (409/619) Branch coverage: 57.3% (171/298) Total lines: 1745 Tag: Kestrun/Kestrun@7a3839f4de2254e22daae81ab8dc7cb2f40c833012/18/2025 - 21:41:58 Line coverage: 69.5% (538/773) Branch coverage: 59.1% (201/340) Total lines: 2177 Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: get_Builder()100%11100%
File 1: get_App()100%22100%
File 1: get_ApplicationName()100%22100%
File 1: get_Options()100%11100%
File 1: .ctor(...)75%4482.14%
File 1: get_IsConfigured()100%11100%
File 1: get_StartTime()100%11100%
File 1: get_StopTime()100%11100%
File 1: get_Uptime()0%4260%
File 1: get_RunspacePool()50%22100%
File 1: get_FeatureQueue()100%11100%
File 1: get_HealthProbes()100%11100%
File 1: get_KestrunRoot()100%11100%
File 1: get_ModulePaths()100%210%
File 1: get_SharedState()100%11100%
File 1: get_Logger()100%11100%
File 1: get_Scheduler()50%22100%
File 1: set_Scheduler(...)100%11100%
File 1: get_Tasks()0%620%
File 1: set_Tasks(...)100%210%
File 1: get_RouteGroupStack()100%11100%
File 1: get_RegisteredRoutes()100%210%
File 1: get_RegisteredAuthentications()100%11100%
File 1: get_DefaultCacheControl()100%11100%
File 1: get_PowershellMiddlewareEnabled()100%11100%
File 1: get_DefaultHost()100%11100%
File 1: get_DefinedCorsPolicyNames()100%11100%
File 1: get_CorsPolicyDefined()100%11100%
File 1: get_StatusCodeOptions()100%11100%
File 1: set_StatusCodeOptions(...)0%620%
File 1: get_ExceptionOptions()100%11100%
File 1: set_ExceptionOptions(...)50%2275%
File 1: get_ForwardedHeaderOptions()100%11100%
File 1: set_ForwardedHeaderOptions(...)100%22100%
File 1: get_AntiforgeryOptions()100%210%
File 1: get_OpenApiDocumentDescriptor()100%11100%
File 1: .ctor(...)100%11100%
File 1: .ctor(...)100%210%
File 1: CreateWebAppOptions()100%22100%
File 1: GetOrCreateOpenApiDocument(...)50%8662.5%
File 1: LogConstructorArgs(...)100%22100%
File 1: SetWorkingDirectoryIfNeeded(...)100%44100%
File 1: GetSafeContentRootPath(...)75%44100%
File 1: GetSafeCurrentDirectory()100%11100%
File 1: AddKestrunModulePathIfMissing(...)75%8883.33%
File 1: InitializeOptions(...)100%22100%
File 1: AddUserModulePaths(...)70%111077.77%
File 1: AddProbe(...)100%210%
File 1: AddProbe(...)100%210%
File 1: AddProbe(...)0%2040%
File 1: GetHealthProbesSnapshot()100%210%
File 1: RegisterProbeInternal(...)50%2277.77%
File 1: ConfigureListener(...)90%101094.11%
File 1: ConfigureListener(...)100%11100%
File 1: ConfigureListener(...)100%11100%
File 1: ConfigureListener(...)0%272160%
File 1: ConfigureListener(...)0%110100%
File 1: ValidateConfiguration()100%66100%
File 1: InitializeRunspacePool(...)50%4483.33%
File 1: ConfigureKestrelBase()100%11100%
File 1: ConfigureNamedPipes()75%9433.33%
File 1: ConfigureHttpsAdapter(...)50%5213.33%
File 1: BindListeners(...)68.75%231669.56%
File 1: LogConfiguredEndpoints()25%5457.14%
File 1: HandleConfigurationError(...)100%11100%
File 1: EnableConfiguration(...)83.33%6683.33%
File 1: RegisterDefaultHealthProbes()50%2266.66%
File 1: Build()100%11100%
File 1: ValidateBuilderState()50%2266.66%
File 1: ApplyQueuedServices()100%22100%
File 1: BuildWebApplication()100%11100%
File 1: ConfigureBuiltInMiddleware()100%11100%
File 1: ConfigureRouting()100%44100%
File 1: ConfigureCors()33.33%19628.57%
File 1: ConfigureExceptionHandling()75%9877.77%
File 1: ConfigureForwardedHeaders()66.66%7671.42%
File 1: ConfigureStatusCodePages()33.33%19628.57%
File 1: ConfigurePowerShellRuntime()25%45816.66%
File 1: LogApplicationInfo()100%11100%
File 1: LogPagesDirectory()75%4485.71%
File 1: ApplyQueuedMiddleware()100%22100%
File 1: ApplyFeatures()100%22100%
File 1: IsServiceRegistered(...)50%44100%
File 1: IsServiceRegistered()100%210%
File 1: AddService(...)100%11100%
File 1: Use(...)100%11100%
File 1: AddFeature(...)100%11100%
File 1: AddScheduling(...)100%1212100%
File 1: AddTasks(...)0%210140%
File 1: AddControllers(...)0%620%
File 1: AddPowerShellRuntime(...)100%22100%
File 1: AddSignalR(...)0%4260%
File 1: Run()0%2040%
File 1: StartAsync()100%44100%
File 1: StopAsync()100%4470%
File 1: Stop()66.66%6685.71%
File 1: get_IsRunning()75%44100%
File 1: CreateRunspacePool(...)50%22100%
File 1: LogCreateRunspacePool(...)100%22100%
File 1: BuildInitialSessionState(...)50%2283.33%
File 1: ImportModulePaths(...)100%22100%
File 1: AddOpenApiStartupScript(...)100%44100%
File 1: AddHostVariables(...)100%11100%
File 1: AddSharedVariables(...)50%4222.22%
File 1: AddUserVariables(...)87.5%88100%
File 1: AddUserFunctions(...)100%44100%
File 1: ResolveMaxRunspaces(...)100%44100%
File 1: Dispose()75%88100%
File 2: AddPowerShellRazorPages(...)33.33%7675%
File 2: AddPowerShellRazorPages(...)100%210%
File 2: AddPowerShellRazorPages(...)100%11100%
File 2: AddPowerShellRazorPages()100%11100%
File 2: AddPowerShellRazorPages(...)100%11100%
File 2: AddPowerShellRazorPages(...)100%11100%
File 2: LogAddPowerShellRazorPages(...)50%2266.66%
File 2: LogAddPowerShellRazorPagesService(...)50%2266.66%
File 2: LogAddPowerShellRazorPagesMiddleware(...)50%2266.66%
File 2: LogAddPowerShellRazorPagesMiddlewareAdded(...)50%2266.66%
File 2: ResolvePagesRootPath(...)100%22100%
File 2: ResolveRazorRootDirectory(...)100%22100%
File 2: ConfigureRazorPages(...)100%44100%
File 2: ConfigureRuntimeCompilationReferences(...)100%11100%
File 2: AddLoadedAssemblyReferences(...)100%22100%
File 2: AddSharedFrameworkReferences(...)100%22100%
File 2: AddPagesFileProviderIfExists(...)100%22100%
File 2: MapPowerShellRazorPages(...)50%3241.66%
File 2: AddRazorPages(...)50%6680%
File 2: AddRazorPages(...)75%4492.85%
File 2: IsManaged(...)100%11100%

File(s)

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

#LineLine coverage
 1using Microsoft.AspNetCore.Server.Kestrel.Core;
 2using System.Net;
 3using System.Management.Automation;
 4using System.Management.Automation.Runspaces;
 5using Kestrun.Utilities;
 6using Microsoft.CodeAnalysis;
 7using System.Reflection;
 8using System.Security.Cryptography.X509Certificates;
 9using Serilog;
 10using Serilog.Events;
 11using Microsoft.AspNetCore.SignalR;
 12using Kestrun.Scheduling;
 13using Kestrun.Middleware;
 14using Kestrun.Scripting;
 15using Kestrun.Hosting.Options;
 16using System.Runtime.InteropServices;
 17using Microsoft.PowerShell;
 18using System.Net.Sockets;
 19using Microsoft.Net.Http.Headers;
 20using Kestrun.Authentication;
 21using Kestrun.Health;
 22using Kestrun.Tasks;
 23using Kestrun.Runtime;
 24using Kestrun.OpenApi;
 25using Microsoft.AspNetCore.Antiforgery;
 26
 27namespace Kestrun.Hosting;
 28
 29/// <summary>
 30/// Provides hosting and configuration for the Kestrun application, including service registration, middleware setup, an
 31/// </summary>
 32public partial class KestrunHost : IDisposable
 33{
 34    #region Fields
 185135    internal WebApplicationBuilder Builder { get; }
 36
 37    private WebApplication? _app;
 38
 13039    internal WebApplication App => _app ?? throw new InvalidOperationException("WebApplication is not built yet. Call Bu
 40
 41    /// <summary>
 42    /// Gets the application name for the Kestrun host.
 43    /// </summary>
 244    public string ApplicationName => Options.ApplicationName ?? "KestrunApp";
 45
 46    /// <summary>
 47    /// Gets the configuration options for the Kestrun host.
 48    /// </summary>
 150849    public KestrunOptions Options { get; private set; } = new();
 50
 51    /// <summary>
 52    /// List of PowerShell module paths to be loaded.
 53    /// </summary>
 48654    private readonly List<string> _modulePaths = [];
 55
 56    /// <summary>
 57    /// Indicates whether the Kestrun host is stopping.
 58    /// </summary>
 59    private int _stopping; // 0 = running, 1 = stopping
 60
 61    /// <summary>
 62    /// Indicates whether the Kestrun host configuration has been applied.
 63    /// </summary>
 37164    public bool IsConfigured { get; private set; }
 65
 66    /// <summary>
 67    /// Gets the timestamp when the Kestrun host was started.
 68    /// </summary>
 1769    public DateTime? StartTime { get; private set; }
 70
 71    /// <summary>
 72    /// Gets the timestamp when the Kestrun host was stopped.
 73    /// </summary>
 1874    public DateTime? StopTime { get; private set; }
 75
 76    /// <summary>
 77    /// Gets the uptime duration of the Kestrun host.
 78    /// While running (no StopTime yet), this returns DateTime.UtcNow - StartTime.
 79    /// After stopping, it returns StopTime - StartTime.
 80    /// If StartTime is not set, returns null.
 81    /// </summary>
 82    public TimeSpan? Uptime =>
 083        !StartTime.HasValue
 084            ? null
 085            : StopTime.HasValue
 086                ? StopTime - StartTime
 087                : DateTime.UtcNow - StartTime.Value;
 88    /// <summary>
 89    /// The runspace pool manager for PowerShell execution.
 90    /// </summary>
 91    private KestrunRunspacePoolManager? _runspacePool;
 92
 93    /// <summary>
 94    /// Status code options for configuring status code pages.
 95    /// </summary>
 96    private StatusCodeOptions? _statusCodeOptions;
 97    /// <summary>
 98    /// Exception options for configuring exception handling.
 99    /// </summary>
 100    private ExceptionOptions? _exceptionOptions;
 101    /// <summary>
 102    /// Forwarded headers options for configuring forwarded headers handling.
 103    /// </summary>
 104    private ForwardedHeadersOptions? _forwardedHeaderOptions;
 105
 8106    internal KestrunRunspacePoolManager RunspacePool => _runspacePool ?? throw new InvalidOperationException("Runspace p
 107
 108    // ── ✦ QUEUE #1 : SERVICE REGISTRATION ✦ ─────────────────────────────
 486109    private readonly List<Action<IServiceCollection>> _serviceQueue = [];
 110
 111    // ── ✦ QUEUE #2 : MIDDLEWARE STAGES ✦ ────────────────────────────────
 486112    private readonly List<Action<IApplicationBuilder>> _middlewareQueue = [];
 113
 590114    internal List<Action<KestrunHost>> FeatureQueue { get; } = [];
 115
 654116    internal List<IProbe> HealthProbes { get; } = [];
 117#if NET9_0_OR_GREATER
 118    private readonly Lock _healthProbeLock = new();
 119#else
 486120    private readonly object _healthProbeLock = new();
 121#endif
 122
 486123    internal readonly Dictionary<(string Pattern, HttpVerb Method), MapRouteOptions> _registeredRoutes =
 486124    new(new RouteKeyComparer());
 125
 126    //internal readonly Dictionary<(string Scheme, string Type), AuthenticationSchemeOptions> _registeredAuthentications
 127    //  new(new AuthKeyComparer());
 128
 129    /// <summary>
 130    /// Gets the root directory path for the Kestrun application.
 131    /// </summary>
 137132    public string? KestrunRoot { get; private set; }
 133
 134    /// <summary>
 135    /// Gets the collection of module paths to be loaded by the Kestrun host.
 136    /// </summary>
 0137    public List<string> ModulePaths => _modulePaths;
 138
 139    /// <summary>
 140    /// Gets the shared state store for managing shared data across requests and sessions.
 141    /// </summary>
 201142    public SharedState.SharedState SharedState { get; }
 143
 144    /// <summary>
 145    /// Gets the Serilog logger instance used by the Kestrun host.
 146    /// </summary>
 9685147    public Serilog.ILogger Logger { get; private set; }
 148
 149    private SchedulerService? _scheduler;
 150    /// <summary>
 151    /// Gets the scheduler service used for managing scheduled tasks in the Kestrun host.
 152    /// Initialized in ConfigureServices via AddScheduler()
 153    /// </summary>
 154    public SchedulerService Scheduler
 155    {
 1156        get => _scheduler ?? throw new InvalidOperationException("SchedulerService is not initialized. Call AddScheduler
 1157        internal set => _scheduler = value;
 158    }
 159
 160    private KestrunTaskService? _tasks;
 161    /// <summary>
 162    /// Gets the ad-hoc task service used for running one-off tasks (PowerShell, C#, VB.NET).
 163    /// Initialized via AddTasks()
 164    /// </summary>
 165    public KestrunTaskService Tasks
 166    {
 0167        get => _tasks ?? throw new InvalidOperationException("Tasks is not initialized. Call AddTasks() to enable task m
 0168        internal set => _tasks = value;
 169    }
 170
 171    /// <summary>
 172    /// Gets the stack used for managing route groups in the Kestrun host.
 173    /// </summary>
 486174    public System.Collections.Stack RouteGroupStack { get; } = new();
 175
 176    /// <summary>
 177    /// Gets the registered routes in the Kestrun host.
 178    /// </summary>
 0179    public Dictionary<(string, HttpVerb), MapRouteOptions> RegisteredRoutes => _registeredRoutes;
 180
 181    /// <summary>
 182    /// Gets the registered authentication schemes in the Kestrun host.
 183    /// </summary>
 506184    public AuthenticationRegistry RegisteredAuthentications { get; } = new();
 185
 186    /// <summary>
 187    /// Gets or sets the default cache control settings for HTTP responses.
 188    /// </summary>
 9189    public CacheControlHeaderValue? DefaultCacheControl { get; internal set; }
 190
 191    /// <summary>
 192    /// Gets the shared state manager for managing shared data across requests and sessions.
 193    /// </summary>
 121194    public bool PowershellMiddlewareEnabled { get; set; } = false;
 195
 196    /// <summary>
 197    /// Gets or sets a value indicating whether this instance is the default Kestrun host.
 198    /// </summary>
 1199    public bool DefaultHost { get; internal set; }
 200
 201    /// <summary>
 202    /// The list of CORS policy names that have been defined in the KestrunHost instance.
 203    /// </summary>
 661204    public List<string> DefinedCorsPolicyNames { get; } = [];
 205
 206    /// <summary>
 207    /// Gets or sets a value indicating whether CORS (Cross-Origin Resource Sharing) is enabled.
 208    /// </summary>
 147209    public bool CorsPolicyDefined => DefinedCorsPolicyNames.Count > 0;
 210
 211    /// <summary>
 212    /// Gets or sets the status code options for configuring status code pages.
 213    /// </summary>
 214    public StatusCodeOptions? StatusCodeOptions
 215    {
 101216        get => _statusCodeOptions;
 217        set
 218        {
 0219            if (IsConfigured)
 220            {
 0221                throw new InvalidOperationException("Cannot modify StatusCodeOptions after configuration is applied.");
 222            }
 0223            _statusCodeOptions = value;
 0224        }
 225    }
 226
 227    /// <summary>
 228    /// Gets or sets the exception options for configuring exception handling.
 229    /// </summary>
 230    public ExceptionOptions? ExceptionOptions
 231    {
 112232        get => _exceptionOptions;
 233        set
 234        {
 5235            if (IsConfigured)
 236            {
 0237                throw new InvalidOperationException("Cannot modify ExceptionOptions after configuration is applied.");
 238            }
 5239            _exceptionOptions = value;
 5240        }
 241    }
 242
 243    /// <summary>
 244    /// Gets or sets the forwarded headers options for configuring forwarded headers handling.
 245    /// </summary>
 246    public ForwardedHeadersOptions? ForwardedHeaderOptions
 247    {
 104248        get => _forwardedHeaderOptions;
 249        set
 250        {
 4251            if (IsConfigured)
 252            {
 1253                throw new InvalidOperationException("Cannot modify ForwardedHeaderOptions after configuration is applied
 254            }
 3255            _forwardedHeaderOptions = value;
 3256        }
 257    }
 258
 259    /// <summary>
 260    /// Gets the antiforgery options for configuring antiforgery token generation and validation.
 261    /// </summary>
 0262    public AntiforgeryOptions? AntiforgeryOptions { get; set; }
 263
 264    /// <summary>
 265    /// Gets the OpenAPI document descriptor for configuring OpenAPI generation.
 266    /// </summary>
 534267    public Dictionary<string, OpenApiDocDescriptor> OpenApiDocumentDescriptor { get; } = [];
 268
 269    #endregion
 270
 271    // Accepts optional module paths (from PowerShell)
 272    #region Constructor
 273
 274    /// <summary>
 275    /// Initializes a new instance of the <see cref="KestrunHost"/> class with the specified application name, root dire
 276    /// </summary>
 277    /// <param name="appName">The name of the application.</param>
 278    /// <param name="kestrunRoot">The root directory for the Kestrun application.</param>
 279    /// <param name="modulePathsObj">An array of module paths to be loaded.</param>
 280    public KestrunHost(string? appName, string? kestrunRoot = null, string[]? modulePathsObj = null) :
 99281            this(appName, Log.Logger, kestrunRoot, modulePathsObj)
 99282    { }
 283
 284    /// <summary>
 285    /// Initializes a new instance of the <see cref="KestrunHost"/> class with the specified application name and logger
 286    /// </summary>
 287    /// <param name="appName">The name of the application.</param>
 288    /// <param name="logger">The Serilog logger instance to use.</param>
 289    /// <param name="ordinalIgnoreCase">Indicates whether the shared state should be case-insensitive.</param>
 290    public KestrunHost(string? appName, Serilog.ILogger logger,
 0291          bool ordinalIgnoreCase) : this(appName, logger, null, null, null, ordinalIgnoreCase)
 0292    { }
 293
 294    /// <summary>
 295    /// Initializes a new instance of the <see cref="KestrunHost"/> class with the specified application name, logger, r
 296    /// </summary>
 297    /// <param name="appName">The name of the application.</param>
 298    /// <param name="logger">The Serilog logger instance to use.</param>
 299    /// <param name="kestrunRoot">The root directory for the Kestrun application.</param>
 300    /// <param name="modulePathsObj">An array of module paths to be loaded.</param>
 301    /// <param name="args">Command line arguments to pass to the application.</param>
 302    /// <param name="ordinalIgnoreCase">Indicates whether the shared state should be case-insensitive.</param>
 486303    public KestrunHost(string? appName, Serilog.ILogger logger,
 486304    string? kestrunRoot = null, string[]? modulePathsObj = null, string[]? args = null, bool ordinalIgnoreCase = true)
 305    {
 306        // ① Logger
 486307        Logger = logger ?? Log.Logger;
 486308        LogConstructorArgs(appName, logger == null, kestrunRoot, modulePathsObj?.Length ?? 0);
 486309        SharedState = new(ordinalIgnoreCase: ordinalIgnoreCase);
 310        // ② Working directory/root
 486311        SetWorkingDirectoryIfNeeded(kestrunRoot);
 312
 313        // ③ Ensure Kestrun module path is available
 486314        AddKestrunModulePathIfMissing(modulePathsObj);
 315
 316        // ④ WebApplicationBuilder
 317        // NOTE:
 318        // ASP.NET Core's WebApplicationBuilder validates that ContentRootPath exists.
 319        // On Unix/macOS, the process current working directory (CWD) can be deleted by tests or external code.
 320        // If we derive ContentRootPath from a missing/deleted directory, CreateBuilder throws.
 321        // We therefore (a) choose an existing directory when possible and (b) retry with a stable fallback
 322        // to keep host creation resilient in CI where test ordering/parallelism can surface this.
 323        WebApplicationOptions CreateWebAppOptions(string contentRootPath)
 324        {
 486325            return new()
 486326            {
 486327                ContentRootPath = contentRootPath,
 486328                Args = args ?? [],
 486329                EnvironmentName = EnvironmentHelper.Name
 486330            };
 331        }
 332
 486333        var contentRootPath = GetSafeContentRootPath(kestrunRoot);
 334
 335        try
 336        {
 486337            Builder = WebApplication.CreateBuilder(CreateWebAppOptions(contentRootPath));
 486338        }
 0339        catch (ArgumentException ex) when (
 0340            string.Equals(ex.ParamName, "contentRootPath", StringComparison.OrdinalIgnoreCase) &&
 0341            !string.Equals(contentRootPath, AppContext.BaseDirectory, StringComparison.Ordinal))
 342        {
 343            // The selected content root may have been deleted between resolution and builder initialization
 344            // (TOCTOU race) or the process CWD may have become invalid. Fall back to a stable path so host
 345            // creation does not fail.
 0346            Builder = WebApplication.CreateBuilder(CreateWebAppOptions(AppContext.BaseDirectory));
 0347        }
 348        // Enable Serilog for the host
 486349        _ = Builder.Host.UseSerilog();
 350
 351        // Make this KestrunHost available via DI so framework-created components (e.g., auth handlers)
 352        // can resolve it. We register the current instance as a singleton.
 486353        _ = Builder.Services.AddSingleton(this);
 354
 355        // Expose Serilog.ILogger via DI for components (e.g., SignalR hubs) that depend on Serilog's logger
 356        // ASP.NET Core registers Microsoft.Extensions.Logging.ILogger by default; we also bind Serilog.ILogger
 357        // to the same instance so constructors like `KestrunHub(Serilog.ILogger logger)` resolve properly.
 486358        _ = Builder.Services.AddSingleton(Logger);
 359
 360        // ⑤ Options
 486361        InitializeOptions(appName);
 362
 363        // ⑥ Add user-provided module paths
 486364        AddUserModulePaths(modulePathsObj);
 365
 486366        Logger.Information("Current working directory: {CurrentDirectory}", GetSafeCurrentDirectory());
 486367    }
 368    #endregion
 369
 370    #region Helpers
 371
 372    /// <summary>
 373    /// Gets the OpenAPI document descriptor for the specified document ID.
 374    /// </summary>
 375    /// <param name="docId">The ID of the OpenAPI document.</param>
 376    /// <returns>The OpenAPI document descriptor.</returns>
 377    public OpenApiDocDescriptor GetOrCreateOpenApiDocument(string docId)
 378    {
 24379        if (string.IsNullOrWhiteSpace(docId))
 380        {
 0381            throw new ArgumentException("Document ID cannot be null or whitespace.", nameof(docId));
 382        }
 383        // Check if descriptor already exists
 24384        if (OpenApiDocumentDescriptor.TryGetValue(docId, out var descriptor))
 385        {
 0386            if (Logger.IsEnabled(LogEventLevel.Debug))
 387            {
 0388                Logger.Debug("OpenAPI document descriptor for ID '{DocId}' already exists. Returning existing descriptor
 389            }
 390        }
 391        else
 392        {
 24393            descriptor = new OpenApiDocDescriptor(this, docId);
 24394            OpenApiDocumentDescriptor[docId] = descriptor;
 395        }
 24396        return descriptor;
 397    }
 398
 399    /// <summary>
 400    /// Logs constructor arguments at Debug level for diagnostics.
 401    /// </summary>
 402    private void LogConstructorArgs(string? appName, bool defaultLogger, string? kestrunRoot, int modulePathsLength)
 403    {
 486404        if (Logger.IsEnabled(LogEventLevel.Debug))
 405        {
 326406            Logger.Debug(
 326407                "KestrunHost ctor: AppName={AppName}, DefaultLogger={DefaultLogger}, KestrunRoot={KestrunRoot}, ModulePa
 326408                appName, defaultLogger, kestrunRoot, modulePathsLength);
 409        }
 486410    }
 411
 412    /// <summary>
 413    /// Sets the current working directory to the provided Kestrun root if needed and stores it.
 414    /// </summary>
 415    /// <param name="kestrunRoot">The Kestrun root directory path.</param>
 416    private void SetWorkingDirectoryIfNeeded(string? kestrunRoot)
 417    {
 486418        if (string.IsNullOrWhiteSpace(kestrunRoot))
 419        {
 350420            return;
 421        }
 422
 136423        if (!string.Equals(GetSafeCurrentDirectory(), kestrunRoot, StringComparison.Ordinal))
 424        {
 99425            Directory.SetCurrentDirectory(kestrunRoot);
 99426            Logger.Information("Changed current directory to Kestrun root: {KestrunRoot}", kestrunRoot);
 427        }
 428        else
 429        {
 37430            Logger.Verbose("Current directory is already set to Kestrun root: {KestrunRoot}", kestrunRoot);
 431        }
 432
 136433        KestrunRoot = kestrunRoot;
 136434    }
 435
 436    private static string GetSafeContentRootPath(string? kestrunRoot)
 437    {
 486438        var candidate = !string.IsNullOrWhiteSpace(kestrunRoot)
 486439            ? kestrunRoot
 486440            : GetSafeCurrentDirectory();
 441
 442        // WebApplication.CreateBuilder requires that ContentRootPath exists.
 443        // On Unix/macOS, getcwd() can fail (or return a path that was deleted) if the CWD was removed.
 444        // This can happen in tests that use temp directories and delete them after constructing a host.
 445        // Guard here to avoid injecting a non-existent content root into ASP.NET Core.
 486446        return Directory.Exists(candidate)
 486447            ? candidate
 486448            : AppContext.BaseDirectory;
 449    }
 450
 451    private static string GetSafeCurrentDirectory()
 452    {
 453        try
 454        {
 1073455            return Directory.GetCurrentDirectory();
 456        }
 2457        catch (Exception ex) when (
 2458            ex is IOException or
 2459            UnauthorizedAccessException or
 2460            DirectoryNotFoundException or
 2461            FileNotFoundException)
 462        {
 463            // On Unix/macOS, getcwd() can fail with ENOENT if the CWD was deleted.
 464            // Fall back to the app base directory to keep host creation resilient.
 2465            return AppContext.BaseDirectory;
 466        }
 1073467    }
 468
 469    /// <summary>
 470    /// Ensures the core Kestrun module path is present; if missing, locates and adds it.
 471    /// </summary>
 472    /// <param name="modulePathsObj">The array of module paths to check.</param>
 473    private void AddKestrunModulePathIfMissing(string[]? modulePathsObj)
 474    {
 486475        var needsLocate = modulePathsObj is null ||
 523476                          (modulePathsObj?.Any(p => p.Contains("Kestrun.psm1", StringComparison.Ordinal)) == false);
 486477        if (!needsLocate)
 478        {
 37479            return;
 480        }
 481
 449482        var kestrunModulePath = PowerShellModuleLocator.LocateKestrunModule();
 449483        if (string.IsNullOrWhiteSpace(kestrunModulePath))
 484        {
 0485            Logger.Fatal("Kestrun module not found. Ensure the Kestrun module is installed.");
 0486            throw new FileNotFoundException("Kestrun module not found.");
 487        }
 488
 449489        Logger.Information("Found Kestrun module at: {KestrunModulePath}", kestrunModulePath);
 449490        Logger.Verbose("Adding Kestrun module path: {KestrunModulePath}", kestrunModulePath);
 449491        _modulePaths.Add(kestrunModulePath);
 449492    }
 493
 494    /// <summary>
 495    /// Initializes Kestrun options and sets the application name when provided.
 496    /// </summary>
 497    /// <param name="appName">The name of the application.</param>
 498    private void InitializeOptions(string? appName)
 499    {
 486500        if (string.IsNullOrEmpty(appName))
 501        {
 1502            Logger.Information("No application name provided, using default.");
 1503            Options = new KestrunOptions();
 504        }
 505        else
 506        {
 485507            Logger.Information("Setting application name: {AppName}", appName);
 485508            Options = new KestrunOptions { ApplicationName = appName };
 509        }
 485510    }
 511
 512    /// <summary>
 513    /// Adds user-provided module paths if they exist, logging warnings for invalid entries.
 514    /// </summary>
 515    /// <param name="modulePathsObj">The array of module paths to check.</param>
 516    private void AddUserModulePaths(string[]? modulePathsObj)
 517    {
 486518        if (modulePathsObj is IEnumerable<object> modulePathsEnum)
 519        {
 148520            foreach (var modPathObj in modulePathsEnum)
 521            {
 37522                if (modPathObj is string modPath && !string.IsNullOrWhiteSpace(modPath))
 523                {
 37524                    if (File.Exists(modPath))
 525                    {
 37526                        Logger.Information("[KestrunHost] Adding module path: {ModPath}", modPath);
 37527                        _modulePaths.Add(modPath);
 528                    }
 529                    else
 530                    {
 0531                        Logger.Warning("[KestrunHost] Module path does not exist: {ModPath}", modPath);
 532                    }
 533                }
 534                else
 535                {
 0536                    Logger.Warning("[KestrunHost] Invalid module path provided.");
 537                }
 538            }
 539        }
 486540    }
 541    #endregion
 542
 543    #region Health Probes
 544
 545    /// <summary>
 546    /// Registers the provided <see cref="IProbe"/> instance with the host.
 547    /// </summary>
 548    /// <param name="probe">The probe to register.</param>
 549    /// <returns>The current <see cref="KestrunHost"/> instance.</returns>
 550    public KestrunHost AddProbe(IProbe probe)
 551    {
 0552        ArgumentNullException.ThrowIfNull(probe);
 0553        RegisterProbeInternal(probe);
 0554        return this;
 555    }
 556
 557    /// <summary>
 558    /// Registers a delegate-based probe.
 559    /// </summary>
 560    /// <param name="name">Probe name.</param>
 561    /// <param name="tags">Optional tag list used for filtering.</param>
 562    /// <param name="callback">Delegate executed when the probe runs.</param>
 563    /// <returns>The current <see cref="KestrunHost"/> instance.</returns>
 564    public KestrunHost AddProbe(string name, string[]? tags, Func<CancellationToken, Task<ProbeResult>> callback)
 565    {
 0566        ArgumentException.ThrowIfNullOrEmpty(name);
 0567        ArgumentNullException.ThrowIfNull(callback);
 568
 0569        var probe = new DelegateProbe(name, tags, callback);
 0570        RegisterProbeInternal(probe);
 0571        return this;
 572    }
 573
 574    /// <summary>
 575    /// Registers a script-based probe written in any supported language.
 576    /// </summary>
 577    /// <param name="name">Probe name.</param>
 578    /// <param name="tags">Optional tag list used for filtering.</param>
 579    /// <param name="code">Script contents.</param>
 580    /// <param name="language">Optional language override. When null, <see cref="KestrunOptions.Health"/> defaults are u
 581    /// <param name="arguments">Optional argument dictionary exposed to the script.</param>
 582    /// <param name="extraImports">Optional language-specific imports.</param>
 583    /// <param name="extraRefs">Optional additional assembly references.</param>
 584    /// <returns>The current <see cref="KestrunHost"/> instance.</returns>
 585    public KestrunHost AddProbe(
 586        string name,
 587        string[]? tags,
 588        string code,
 589        ScriptLanguage? language = null,
 590        IReadOnlyDictionary<string, object?>? arguments = null,
 591        string[]? extraImports = null,
 592        Assembly[]? extraRefs = null)
 593    {
 0594        ArgumentException.ThrowIfNullOrEmpty(name);
 0595        ArgumentException.ThrowIfNullOrEmpty(code);
 596
 0597        var effectiveLanguage = language ?? Options.Health.DefaultScriptLanguage;
 0598        var logger = Logger.ForContext("HealthProbe", name);
 0599        var probe = ScriptProbeFactory.Create(host: this, name: name, tags: tags,
 0600            effectiveLanguage, code: code,
 0601            runspaceAccessor: effectiveLanguage == ScriptLanguage.PowerShell ? () => RunspacePool : null,
 0602            arguments: arguments, extraImports: extraImports, extraRefs: extraRefs);
 603
 0604        RegisterProbeInternal(probe);
 0605        return this;
 606    }
 607
 608    /// <summary>
 609    /// Returns a snapshot of the currently registered probes.
 610    /// </summary>
 611    internal IReadOnlyList<IProbe> GetHealthProbesSnapshot()
 612    {
 0613        lock (_healthProbeLock)
 614        {
 0615            return [.. HealthProbes];
 616        }
 0617    }
 618
 619    private void RegisterProbeInternal(IProbe probe)
 620    {
 56621        lock (_healthProbeLock)
 622        {
 56623            var index = HealthProbes.FindIndex(p => string.Equals(p.Name, probe.Name, StringComparison.OrdinalIgnoreCase
 56624            if (index >= 0)
 625            {
 0626                HealthProbes[index] = probe;
 0627                Logger.Information("Replaced health probe {ProbeName}.", probe.Name);
 628            }
 629            else
 630            {
 56631                HealthProbes.Add(probe);
 56632                Logger.Information("Registered health probe {ProbeName}.", probe.Name);
 633            }
 56634        }
 56635    }
 636
 637    #endregion
 638
 639    #region ListenerOptions
 640
 641    /// <summary>
 642    /// Configures a listener for the Kestrun host with the specified port, optional IP address, certificate, protocols,
 643    /// </summary>
 644    /// <param name="port">The port number to listen on.</param>
 645    /// <param name="ipAddress">The IP address to bind to. If null, binds to any address.</param>
 646    /// <param name="x509Certificate">The X509 certificate for HTTPS. If null, HTTPS is not used.</param>
 647    /// <param name="protocols">The HTTP protocols to use.</param>
 648    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 649    /// <returns>The current KestrunHost instance.</returns>
 650    public KestrunHost ConfigureListener(
 651    int port,
 652    IPAddress? ipAddress = null,
 653    X509Certificate2? x509Certificate = null,
 654    HttpProtocols protocols = HttpProtocols.Http1,
 655    bool useConnectionLogging = false)
 656    {
 37657        if (Logger.IsEnabled(LogEventLevel.Debug))
 658        {
 18659            Logger.Debug("ConfigureListener port={Port}, ipAddress={IPAddress}, protocols={Protocols}, useConnectionLogg
 660        }
 661        // Validate state
 37662        if (IsConfigured)
 663        {
 0664            throw new InvalidOperationException("Cannot configure listeners after configuration is applied.");
 665        }
 666        // Validate protocols
 37667        if (protocols == HttpProtocols.Http1AndHttp2AndHttp3 && !CcUtilities.PreviewFeaturesEnabled())
 668        {
 2669            Logger.Warning("Http3 is not supported in this version of Kestrun. Using Http1 and Http2 only.");
 2670            protocols = HttpProtocols.Http1AndHttp2;
 671        }
 672        // Add listener
 37673        Options.Listeners.Add(new ListenerOptions
 37674        {
 37675            IPAddress = ipAddress ?? IPAddress.Any,
 37676            Port = port,
 37677            UseHttps = x509Certificate != null,
 37678            X509Certificate = x509Certificate,
 37679            Protocols = protocols,
 37680            UseConnectionLogging = useConnectionLogging
 37681        });
 37682        return this;
 683    }
 684
 685    /// <summary>
 686    /// Configures a listener for the Kestrun host with the specified port, optional IP address, and connection logging.
 687    /// </summary>
 688    /// <param name="port">The port number to listen on.</param>
 689    /// <param name="ipAddress">The IP address to bind to. If null, binds to any address.</param>
 690    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 691    public void ConfigureListener(
 692    int port,
 693    IPAddress? ipAddress = null,
 20694    bool useConnectionLogging = false) => _ = ConfigureListener(port: port, ipAddress: ipAddress, x509Certificate: null,
 695
 696    /// <summary>
 697    /// Configures a listener for the Kestrun host with the specified port and connection logging option.
 698    /// </summary>
 699    /// <param name="port">The port number to listen on.</param>
 700    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 701    public void ConfigureListener(
 702    int port,
 1703    bool useConnectionLogging = false) => _ = ConfigureListener(port: port, ipAddress: null, x509Certificate: null, prot
 704
 705    /// <summary>
 706    /// Configures listeners for the Kestrun host by resolving the specified host name to IP addresses and binding to ea
 707    /// </summary>
 708    /// <param name="hostName">The host name to resolve and bind to.</param>
 709    /// <param name="port">The port number to listen on.</param>
 710    /// <param name="x509Certificate">The X509 certificate for HTTPS. If null, HTTPS is not used.</param>
 711    /// <param name="protocols">The HTTP protocols to use.</param>
 712    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 713    /// <param name="families">Optional array of address families to filter resolved addresses (e.g., IPv4-only).</param
 714    /// <returns>The current KestrunHost instance.</returns>
 715    /// <exception cref="ArgumentException">Thrown when the host name is null or whitespace.</exception>
 716    /// <exception cref="InvalidOperationException">Thrown when no valid IP addresses are resolved.</exception>
 717    public KestrunHost ConfigureListener(
 718    string hostName,
 719    int port,
 720    X509Certificate2? x509Certificate = null,
 721    HttpProtocols protocols = HttpProtocols.Http1,
 722    bool useConnectionLogging = false,
 723    AddressFamily[]? families = null) // e.g. new[] { AddressFamily.InterNetwork } for IPv4-only
 724    {
 0725        if (string.IsNullOrWhiteSpace(hostName))
 726        {
 0727            throw new ArgumentException("Host name must be provided.", nameof(hostName));
 728        }
 729
 730        // If caller passed an IP literal, just bind once.
 0731        if (IPAddress.TryParse(hostName, out var parsedIp))
 732        {
 0733            _ = ConfigureListener(port, parsedIp, x509Certificate, protocols, useConnectionLogging);
 0734            return this;
 735        }
 736
 737        // Resolve and bind to ALL matching addresses (IPv4/IPv6)
 0738        var addrs = Dns.GetHostAddresses(hostName)
 0739                       .Where(a => families is null || families.Length == 0 || families.Contains(a.AddressFamily))
 0740                       .Where(a => a.AddressFamily is AddressFamily.InterNetwork or AddressFamily.InterNetworkV6)
 0741                       .ToArray();
 742
 0743        if (addrs.Length == 0)
 744        {
 0745            throw new InvalidOperationException($"No IPv4/IPv6 addresses resolved for host '{hostName}'.");
 746        }
 747
 0748        foreach (var addr in addrs)
 749        {
 0750            _ = ConfigureListener(port, addr, x509Certificate, protocols, useConnectionLogging);
 751        }
 752
 0753        return this;
 754    }
 755
 756    /// <summary>
 757    /// Configures listeners for the Kestrun host based on the provided absolute URI, resolving the host to IP addresses
 758    /// </summary>
 759    /// <param name="uri">The absolute URI to configure the listener for.</param>
 760    /// <param name="x509Certificate">The X509 certificate for HTTPS. If null, HTTPS is not used.</param>
 761    /// <param name="protocols">The HTTP protocols to use.</param>
 762    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 763    /// <param name="families">Optional array of address families to filter resolved addresses (e.g., IPv4-only).</param
 764    /// <returns>The current KestrunHost instance.</returns>
 765    /// <exception cref="ArgumentException">Thrown when the provided URI is not absolute.</exception>
 766    /// <exception cref="InvalidOperationException">Thrown when no valid IP addresses are resolved.</exception>
 767    public KestrunHost ConfigureListener(
 768    Uri uri,
 769    X509Certificate2? x509Certificate = null,
 770    HttpProtocols? protocols = null,
 771    bool useConnectionLogging = false,
 772    AddressFamily[]? families = null)
 773    {
 0774        ArgumentNullException.ThrowIfNull(uri);
 775
 0776        if (!uri.IsAbsoluteUri)
 777        {
 0778            throw new ArgumentException("URL must be absolute.", nameof(uri));
 779        }
 780
 0781        var isHttps = uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase);
 0782        var port = uri.IsDefaultPort ? (isHttps ? 443 : 80) : uri.Port;
 783
 784        // Default: HTTPS → H1+H2, HTTP → H1
 0785        var chosenProtocols = protocols ?? (isHttps ? HttpProtocols.Http1AndHttp2 : HttpProtocols.Http1);
 786
 787        // Delegate to hostname overload (which will resolve or handle IP literal)
 0788        return ConfigureListener(
 0789            hostName: uri.Host,
 0790            port: port,
 0791            x509Certificate: x509Certificate,
 0792            protocols: chosenProtocols,
 0793            useConnectionLogging: useConnectionLogging,
 0794            families: families
 0795        );
 796    }
 797
 798    #endregion
 799
 800    #region Configuration
 801
 802    /// <summary>
 803    /// Validates if configuration can be applied and returns early if already configured.
 804    /// </summary>
 805    /// <returns>True if configuration should proceed, false if it should be skipped.</returns>
 806    internal bool ValidateConfiguration()
 807    {
 76808        if (Logger.IsEnabled(LogEventLevel.Debug))
 809        {
 41810            Logger.Debug("EnableConfiguration(options) called");
 811        }
 812
 76813        if (IsConfigured)
 814        {
 18815            if (Logger.IsEnabled(LogEventLevel.Debug))
 816            {
 2817                Logger.Debug("Configuration already applied, skipping");
 818            }
 18819            return false; // Already configured
 820        }
 821
 58822        return true;
 823    }
 824
 825    /// <summary>
 826    /// Creates and initializes the runspace pool for PowerShell execution.
 827    /// </summary>
 828    /// <param name="userVariables">User-defined variables to inject into the runspace pool.</param>
 829    /// <param name="userFunctions">User-defined functions to inject into the runspace pool.</param>
 830    /// <param name="openApiClassesPath">Path to the OpenAPI class definitions to inject into the runspace pool.</param>
 831    /// <exception cref="InvalidOperationException">Thrown when runspace pool creation fails.</exception>
 832    internal void InitializeRunspacePool(Dictionary<string, object>? userVariables, Dictionary<string, string>? userFunc
 833    {
 59834        _runspacePool =
 59835            CreateRunspacePool(Options.MaxRunspaces, userVariables, userFunctions, openApiClassesPath) ??
 59836            throw new InvalidOperationException("Failed to create runspace pool.");
 59837        if (Logger.IsEnabled(LogEventLevel.Verbose))
 838        {
 0839            Logger.Verbose("Runspace pool created with max runspaces: {MaxRunspaces}", Options.MaxRunspaces);
 840        }
 59841    }
 842
 843    /// <summary>
 844    /// Configures the Kestrel web server with basic options.
 845    /// </summary>
 846    internal void ConfigureKestrelBase()
 847    {
 57848        _ = Builder.WebHost.UseKestrel(opts =>
 57849        {
 56850            opts.CopyFromTemplate(Options.ServerOptions);
 113851        });
 57852    }
 853
 854    /// <summary>
 855    /// Configures named pipe listeners if supported on the current platform.
 856    /// </summary>
 857    internal void ConfigureNamedPipes()
 858    {
 58859        if (Options.NamedPipeOptions is not null)
 860        {
 1861            if (OperatingSystem.IsWindows())
 862            {
 0863                _ = Builder.WebHost.UseNamedPipes(opts =>
 0864                {
 0865                    opts.ListenerQueueCount = Options.NamedPipeOptions.ListenerQueueCount;
 0866                    opts.MaxReadBufferSize = Options.NamedPipeOptions.MaxReadBufferSize;
 0867                    opts.MaxWriteBufferSize = Options.NamedPipeOptions.MaxWriteBufferSize;
 0868                    opts.CurrentUserOnly = Options.NamedPipeOptions.CurrentUserOnly;
 0869                    opts.PipeSecurity = Options.NamedPipeOptions.PipeSecurity;
 0870                });
 871            }
 872            else
 873            {
 1874                Logger.Verbose("Named pipe listeners configuration is supported only on Windows; skipping UseNamedPipes 
 875            }
 876        }
 58877    }
 878
 879    /// <summary>
 880    /// Configures HTTPS connection adapter defaults.
 881    /// </summary>
 882    /// <param name="serverOptions">The Kestrel server options to configure.</param>
 883    internal void ConfigureHttpsAdapter(KestrelServerOptions serverOptions)
 884    {
 57885        if (Options.HttpsConnectionAdapter is not null)
 886        {
 0887            Logger.Verbose("Applying HTTPS connection adapter options from KestrunOptions.");
 888
 889            // Apply HTTPS defaults if needed
 0890            serverOptions.ConfigureHttpsDefaults(httpsOptions =>
 0891            {
 0892                httpsOptions.SslProtocols = Options.HttpsConnectionAdapter.SslProtocols;
 0893                httpsOptions.ClientCertificateMode = Options.HttpsConnectionAdapter.ClientCertificateMode;
 0894                httpsOptions.ClientCertificateValidation = Options.HttpsConnectionAdapter.ClientCertificateValidation;
 0895                httpsOptions.CheckCertificateRevocation = Options.HttpsConnectionAdapter.CheckCertificateRevocation;
 0896                httpsOptions.ServerCertificate = Options.HttpsConnectionAdapter.ServerCertificate;
 0897                httpsOptions.ServerCertificateChain = Options.HttpsConnectionAdapter.ServerCertificateChain;
 0898                httpsOptions.ServerCertificateSelector = Options.HttpsConnectionAdapter.ServerCertificateSelector;
 0899                httpsOptions.HandshakeTimeout = Options.HttpsConnectionAdapter.HandshakeTimeout;
 0900                httpsOptions.OnAuthenticate = Options.HttpsConnectionAdapter.OnAuthenticate;
 0901            });
 902        }
 57903    }
 904
 905    /// <summary>
 906    /// Binds all configured listeners (Unix sockets, named pipes, TCP) to the server.
 907    /// </summary>
 908    /// <param name="serverOptions">The Kestrel server options to configure.</param>
 909    internal void BindListeners(KestrelServerOptions serverOptions)
 910    {
 911        // Unix domain socket listeners
 116912        foreach (var unixSocket in Options.ListenUnixSockets)
 913        {
 0914            if (!string.IsNullOrWhiteSpace(unixSocket))
 915            {
 0916                Logger.Verbose("Binding Unix socket: {Sock}", unixSocket);
 0917                serverOptions.ListenUnixSocket(unixSocket);
 918                // NOTE: control access via directory perms/umask; UDS file perms are inherited from process umask
 919                // Prefer placing the socket under a group-owned dir (e.g., /var/run/kestrun) with 0770.
 920            }
 921        }
 922
 923        // Named pipe listeners
 116924        foreach (var namedPipeName in Options.NamedPipeNames)
 925        {
 0926            if (!string.IsNullOrWhiteSpace(namedPipeName))
 927            {
 0928                Logger.Verbose("Binding Named Pipe: {Pipe}", namedPipeName);
 0929                serverOptions.ListenNamedPipe(namedPipeName);
 930            }
 931        }
 932
 933        // TCP listeners
 176934        foreach (var opt in Options.Listeners)
 935        {
 30936            serverOptions.Listen(opt.IPAddress, opt.Port, listenOptions =>
 30937            {
 30938                listenOptions.Protocols = opt.Protocols;
 30939                listenOptions.DisableAltSvcHeader = opt.DisableAltSvcHeader;
 30940                if (opt.UseHttps && opt.X509Certificate is not null)
 30941                {
 2942                    _ = listenOptions.UseHttps(opt.X509Certificate);
 30943                }
 30944                if (opt.UseConnectionLogging)
 30945                {
 0946                    _ = listenOptions.UseConnectionLogging();
 30947                }
 60948            });
 949        }
 58950    }
 951
 952    /// <summary>
 953    /// Logs the configured endpoints after building the application.
 954    /// </summary>
 955    internal void LogConfiguredEndpoints()
 956    {
 957        // build the app to validate configuration
 57958        _app = Build();
 959        // Log configured endpoints
 57960        var dataSource = _app.Services.GetRequiredService<EndpointDataSource>();
 961
 57962        if (dataSource.Endpoints.Count == 0)
 963        {
 57964            Logger.Warning("EndpointDataSource is empty. No endpoints configured.");
 965        }
 966        else
 967        {
 0968            foreach (var ep in dataSource.Endpoints)
 969            {
 0970                Logger.Information("➡️  Endpoint: {DisplayName}", ep.DisplayName);
 971            }
 972        }
 0973    }
 974
 975    /// <summary>
 976    /// Handles configuration errors and wraps them with meaningful messages.
 977    /// </summary>
 978    /// <param name="ex">The exception that occurred during configuration.</param>
 979    /// <exception cref="InvalidOperationException">Always thrown with wrapped exception.</exception>
 980    internal void HandleConfigurationError(Exception ex)
 981    {
 1982        Logger.Error(ex, "Error applying configuration: {Message}", ex.Message);
 1983        throw new InvalidOperationException("Failed to apply configuration.", ex);
 984    }
 985
 986    /// <summary>
 987    /// Applies the configured options to the Kestrel server and initializes the runspace pool.
 988    /// </summary>
 989    public void EnableConfiguration(Dictionary<string, object>? userVariables = null, Dictionary<string, string>? userFu
 990    {
 73991        if (!ValidateConfiguration())
 992        {
 17993            return;
 994        }
 995
 996        try
 997        {
 998            // Export OpenAPI classes from PowerShell
 56999            var openApiClassesPath = PowerShellOpenApiClassExporter.ExportOpenApiClasses();
 561000            if (Logger.IsEnabled(LogEventLevel.Debug))
 1001            {
 371002                if (string.IsNullOrWhiteSpace(openApiClassesPath))
 1003                {
 371004                    Logger.Debug("No OpenAPI classes exported from PowerShell.");
 1005                }
 1006                else
 1007                {
 01008                    Logger.Debug("Exported OpenAPI classes from PowerShell: {path}", openApiClassesPath);
 1009                }
 1010            }
 1011            // Initialize PowerShell runspace pool
 561012            InitializeRunspacePool(userVariables: userVariables, userFunctions: userFunctions, openApiClassesPath: openA
 1013            // Configure Kestrel server
 561014            ConfigureKestrelBase();
 1015            // Configure named pipe listeners if any
 561016            ConfigureNamedPipes();
 1017
 1018            // Apply Kestrel listeners and HTTPS settings
 561019            _ = Builder.WebHost.ConfigureKestrel(serverOptions =>
 561020            {
 561021                ConfigureHttpsAdapter(serverOptions);
 561022                BindListeners(serverOptions);
 1121023            });
 1024
 561025            LogConfiguredEndpoints();
 1026
 1027            // Register default probes after endpoints are logged but before marking configured
 561028            RegisterDefaultHealthProbes();
 561029            IsConfigured = true;
 561030            Logger.Information("Configuration applied successfully.");
 561031        }
 01032        catch (Exception ex)
 1033        {
 01034            HandleConfigurationError(ex);
 01035        }
 561036    }
 1037
 1038    /// <summary>
 1039    /// Registers built-in default health probes (idempotent). Currently includes disk space probe.
 1040    /// </summary>
 1041    private void RegisterDefaultHealthProbes()
 1042    {
 1043        try
 1044        {
 1045            // Avoid duplicate registration if user already added a probe named "disk".
 561046            lock (_healthProbeLock)
 1047            {
 561048                if (HealthProbes.Any(p => string.Equals(p.Name, "disk", StringComparison.OrdinalIgnoreCase)))
 1049                {
 01050                    return; // already present
 1051                }
 561052            }
 1053
 561054            var tags = new[] { IProbe.TAG_SELF }; // neutral tag; user can filter by name if needed
 561055            var diskProbe = new DiskSpaceProbe("disk", tags);
 561056            RegisterProbeInternal(diskProbe);
 561057        }
 01058        catch (Exception ex)
 1059        {
 01060            Logger.Warning(ex, "Failed to register default disk space probe.");
 01061        }
 561062    }
 1063
 1064    #endregion
 1065    #region Builder
 1066    /* More information about the KestrunHost class
 1067    https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication?view=aspnetcore-8.0
 1068
 1069    */
 1070
 1071    /// <summary>
 1072    /// Builds the WebApplication.
 1073    /// This method applies all queued services and middleware stages,
 1074    /// and returns the built WebApplication instance.
 1075    /// </summary>
 1076    /// <returns>The built WebApplication.</returns>
 1077    /// <exception cref="InvalidOperationException"></exception>
 1078    public WebApplication Build()
 1079    {
 1011080        ValidateBuilderState();
 1011081        ApplyQueuedServices();
 1011082        BuildWebApplication();
 1011083        ConfigureBuiltInMiddleware();
 1011084        LogApplicationInfo();
 1011085        ApplyQueuedMiddleware();
 1011086        ApplyFeatures();
 1087
 1011088        return _app!;
 1089    }
 1090
 1091    /// <summary>
 1092    /// Validates that the builder is properly initialized before building.
 1093    /// </summary>
 1094    /// <exception cref="InvalidOperationException">Thrown when the builder is not initialized.</exception>
 1095    private void ValidateBuilderState()
 1096    {
 1011097        if (Builder == null)
 1098        {
 01099            throw new InvalidOperationException("Call CreateBuilder() first.");
 1100        }
 1011101    }
 1102
 1103    /// <summary>
 1104    /// Applies all queued service configurations to the service collection.
 1105    /// </summary>
 1106    private void ApplyQueuedServices()
 1107    {
 3341108        foreach (var configure in _serviceQueue)
 1109        {
 661110            configure(Builder.Services);
 1111        }
 1011112    }
 1113
 1114    /// <summary>
 1115    /// Builds the WebApplication instance from the configured builder.
 1116    /// </summary>
 1117    private void BuildWebApplication()
 1118    {
 1011119        _app = Builder.Build();
 1011120        Logger.Information("Application built successfully.");
 1011121    }
 1122
 1123    /// <summary>
 1124    /// Configures built-in middleware components in the correct order.
 1125    /// </summary>
 1126    private void ConfigureBuiltInMiddleware()
 1127    {
 1128        // Configure routing
 1011129        ConfigureRouting();
 1130        // Configure CORS
 1011131        ConfigureCors();
 1132        // Configure exception handling
 1011133        ConfigureExceptionHandling();
 1134        // Configure forwarded headers
 1011135        ConfigureForwardedHeaders();
 1136        // Configure status code pages
 1011137        ConfigureStatusCodePages();
 1138        // Configure PowerShell runtime
 1011139        ConfigurePowerShellRuntime();
 1011140    }
 1141
 1142    /// <summary>
 1143    /// Configures routing middleware.
 1144    /// </summary>
 1145    private void ConfigureRouting()
 1146    {
 1011147        if (Logger.IsEnabled(LogEventLevel.Debug))
 1148        {
 791149            Logger.Debug("Enabling routing middleware.");
 1150        }
 1011151        _ = _app!.UseRouting();
 1011152        if (Logger.IsEnabled(LogEventLevel.Debug))
 1153        {
 791154            Logger.Debug("Routing middleware is enabled.");
 1155        }
 1011156    }
 1157
 1158    /// <summary>
 1159    /// Configures CORS middleware if a CORS policy is defined.
 1160    /// </summary>
 1161    private void ConfigureCors()
 1162    {
 1011163        if (CorsPolicyDefined)
 1164        {
 01165            if (Logger.IsEnabled(LogEventLevel.Debug))
 1166            {
 01167                Logger.Debug("Enabling CORS middleware.");
 1168            }
 01169            _ = _app!.UseCors();
 01170            if (Logger.IsEnabled(LogEventLevel.Debug))
 1171            {
 01172                Logger.Debug("CORS middleware is enabled.");
 1173            }
 1174        }
 1011175    }
 1176
 1177    /// <summary>
 1178    /// Configures exception handling middleware if enabled.
 1179    /// </summary>
 1180    private void ConfigureExceptionHandling()
 1181    {
 1011182        if (ExceptionOptions is not null)
 1183        {
 51184            if (Logger.IsEnabled(LogEventLevel.Debug))
 1185            {
 01186                Logger.Debug("Enabling exception handling middleware.");
 1187            }
 51188            _ = ExceptionOptions.DeveloperExceptionPageOptions is not null
 51189                ? _app!.UseDeveloperExceptionPage(ExceptionOptions.DeveloperExceptionPageOptions)
 51190                : _app!.UseExceptionHandler(ExceptionOptions);
 51191            if (Logger.IsEnabled(LogEventLevel.Debug))
 1192            {
 01193                Logger.Debug("Exception handling middleware is enabled.");
 1194            }
 1195        }
 1011196    }
 1197
 1198    /// <summary>
 1199    /// Configures forwarded headers middleware if enabled.
 1200    /// </summary>
 1201    private void ConfigureForwardedHeaders()
 1202    {
 1011203        if (ForwardedHeaderOptions is not null)
 1204        {
 31205            if (Logger.IsEnabled(LogEventLevel.Debug))
 1206            {
 01207                Logger.Debug("Enabling forwarded headers middleware.");
 1208            }
 31209            _ = _app!.UseForwardedHeaders(ForwardedHeaderOptions);
 31210            if (Logger.IsEnabled(LogEventLevel.Debug))
 1211            {
 01212                Logger.Debug("Forwarded headers middleware is enabled.");
 1213            }
 1214        }
 1011215    }
 1216
 1217    /// <summary>
 1218    /// Configures status code pages middleware if enabled.
 1219    /// </summary>
 1220    private void ConfigureStatusCodePages()
 1221    {
 1222        // Register StatusCodePages BEFORE language runtimes so that re-executed requests
 1223        // pass through language middleware again (and get fresh RouteValues/context).
 1011224        if (StatusCodeOptions is not null)
 1225        {
 01226            if (Logger.IsEnabled(LogEventLevel.Debug))
 1227            {
 01228                Logger.Debug("Enabling status code pages middleware.");
 1229            }
 01230            _ = _app!.UseStatusCodePages(StatusCodeOptions);
 01231            if (Logger.IsEnabled(LogEventLevel.Debug))
 1232            {
 01233                Logger.Debug("Status code pages middleware is enabled.");
 1234            }
 1235        }
 1011236    }
 1237
 1238    /// <summary>
 1239    /// Configures PowerShell runtime middleware if enabled.
 1240    /// </summary>
 1241    /// <exception cref="InvalidOperationException">Thrown when PowerShell is enabled but runspace pool is not initializ
 1242    private void ConfigurePowerShellRuntime()
 1243    {
 1011244        if (PowershellMiddlewareEnabled)
 1245        {
 01246            if (Logger.IsEnabled(LogEventLevel.Debug))
 1247            {
 01248                Logger.Debug("Enabling PowerShell middleware.");
 1249            }
 1250
 01251            if (_runspacePool is null)
 1252            {
 01253                throw new InvalidOperationException("Runspace pool is not initialized. Call EnableConfiguration first.")
 1254            }
 1255
 01256            Logger.Information("Adding PowerShell runtime");
 01257            _ = _app!.UseLanguageRuntime(
 01258                    ScriptLanguage.PowerShell,
 01259                    b => b.UsePowerShellRunspace(_runspacePool));
 1260
 01261            if (Logger.IsEnabled(LogEventLevel.Debug))
 1262            {
 01263                Logger.Debug("PowerShell middleware is enabled.");
 1264            }
 1265        }
 1011266    }
 1267
 1268    /// <summary>
 1269    /// Logs application information including working directory and Pages directory contents.
 1270    /// </summary>
 1271    private void LogApplicationInfo()
 1272    {
 1011273        Logger.Information("CWD: {CWD}", GetSafeCurrentDirectory());
 1011274        Logger.Information("ContentRoot: {Root}", _app!.Environment.ContentRootPath);
 1011275        LogPagesDirectory();
 1011276    }
 1277
 1278    /// <summary>
 1279    /// Logs information about the Pages directory and its contents.
 1280    /// </summary>
 1281    private void LogPagesDirectory()
 1282    {
 1011283        var pagesDir = Path.Combine(_app!.Environment.ContentRootPath, "Pages");
 1011284        Logger.Information("Pages Dir: {PagesDir}", pagesDir);
 1285
 1011286        if (Directory.Exists(pagesDir))
 1287        {
 21288            foreach (var file in Directory.GetFiles(pagesDir, "*.*", SearchOption.AllDirectories))
 1289            {
 01290                Logger.Information("Pages file: {File}", file);
 1291            }
 1292        }
 1293        else
 1294        {
 1001295            Logger.Warning("Pages directory does not exist: {PagesDir}", pagesDir);
 1296        }
 1001297    }
 1298
 1299    /// <summary>
 1300    /// Applies all queued middleware stages to the application pipeline.
 1301    /// </summary>
 1302    private void ApplyQueuedMiddleware()
 1303    {
 2821304        foreach (var stage in _middlewareQueue)
 1305        {
 401306            stage(_app!);
 1307        }
 1011308    }
 1309
 1310    /// <summary>
 1311    /// Applies all queued features to the host.
 1312    /// </summary>
 1313    private void ApplyFeatures()
 1314    {
 2061315        foreach (var feature in FeatureQueue)
 1316        {
 21317            feature(this);
 1318        }
 1011319    }
 1320
 1321    /// <summary>
 1322    /// Returns true if the specified service type has already been registered in the IServiceCollection.
 1323    /// </summary>
 1324    public bool IsServiceRegistered(Type serviceType)
 7911325        => Builder?.Services?.Any(sd => sd.ServiceType == serviceType) ?? false;
 1326
 1327    /// <summary>
 1328    /// Generic convenience overload.
 1329    /// </summary>
 01330    public bool IsServiceRegistered<TService>() => IsServiceRegistered(typeof(TService));
 1331
 1332    /// <summary>
 1333    /// Adds a service configuration action to the service queue.
 1334    /// This action will be executed when the services are built.
 1335    /// </summary>
 1336    /// <param name="configure">The service configuration action.</param>
 1337    /// <returns>The current KestrunHost instance.</returns>
 1338    public KestrunHost AddService(Action<IServiceCollection> configure)
 1339    {
 1291340        _serviceQueue.Add(configure);
 1291341        return this;
 1342    }
 1343
 1344    /// <summary>
 1345    /// Adds a middleware stage to the application pipeline.
 1346    /// </summary>
 1347    /// <param name="stage">The middleware stage to add.</param>
 1348    /// <returns>The current KestrunHost instance.</returns>
 1349    public KestrunHost Use(Action<IApplicationBuilder> stage)
 1350    {
 981351        _middlewareQueue.Add(stage);
 981352        return this;
 1353    }
 1354
 1355    /// <summary>
 1356    /// Adds a feature configuration action to the feature queue.
 1357    /// This action will be executed when the features are applied.
 1358    /// </summary>
 1359    /// <param name="feature">The feature configuration action.</param>
 1360    /// <returns>The current KestrunHost instance.</returns>
 1361    public KestrunHost AddFeature(Action<KestrunHost> feature)
 1362    {
 21363        FeatureQueue.Add(feature);
 21364        return this;
 1365    }
 1366
 1367    /// <summary>
 1368    /// Adds a scheduling feature to the Kestrun host, optionally specifying the maximum number of runspaces for the sch
 1369    /// </summary>
 1370    /// <param name="MaxRunspaces">The maximum number of runspaces for the scheduler. If null, uses the default value.</
 1371    /// <returns>The current KestrunHost instance.</returns>
 1372    public KestrunHost AddScheduling(int? MaxRunspaces = null)
 1373    {
 41374        return MaxRunspaces is not null and <= 0
 41375            ? throw new ArgumentOutOfRangeException(nameof(MaxRunspaces), "MaxRunspaces must be greater than zero.")
 41376            : AddFeature(host =>
 41377        {
 21378            if (Logger.IsEnabled(LogEventLevel.Debug))
 41379            {
 21380                Logger.Debug("AddScheduling (deferred)");
 41381            }
 41382
 21383            if (host._scheduler is null)
 41384            {
 11385                if (MaxRunspaces is not null and > 0)
 41386                {
 11387                    Logger.Information("Setting MaxSchedulerRunspaces to {MaxRunspaces}", MaxRunspaces);
 11388                    host.Options.MaxSchedulerRunspaces = MaxRunspaces.Value;
 41389                }
 11390                Logger.Verbose("Creating SchedulerService with MaxSchedulerRunspaces={MaxRunspaces}",
 11391                    host.Options.MaxSchedulerRunspaces);
 11392                var pool = host.CreateRunspacePool(host.Options.MaxSchedulerRunspaces);
 11393                var logger = Logger.ForContext<KestrunHost>();
 11394                host.Scheduler = new SchedulerService(pool, logger);
 41395            }
 41396            else
 41397            {
 11398                Logger.Warning("SchedulerService already configured; skipping.");
 41399            }
 51400        });
 1401    }
 1402
 1403    /// <summary>
 1404    /// Adds the Tasks feature to run ad-hoc scripts with status/result/cancellation.
 1405    /// </summary>
 1406    /// <param name="MaxRunspaces">Optional max runspaces for the task PowerShell pool; when null uses scheduler default
 1407    /// <returns>The current KestrunHost instance.</returns>
 1408    public KestrunHost AddTasks(int? MaxRunspaces = null)
 1409    {
 01410        return MaxRunspaces is not null and <= 0
 01411            ? throw new ArgumentOutOfRangeException(nameof(MaxRunspaces), "MaxRunspaces must be greater than zero.")
 01412            : AddFeature(host =>
 01413        {
 01414            if (Logger.IsEnabled(LogEventLevel.Debug))
 01415            {
 01416                Logger.Debug("AddTasks (deferred)");
 01417            }
 01418
 01419            if (host._tasks is null)
 01420            {
 01421                // Reuse scheduler pool sizing unless explicitly overridden
 01422                if (MaxRunspaces is not null and > 0)
 01423                {
 01424                    Logger.Information("Setting MaxTaskRunspaces to {MaxRunspaces}", MaxRunspaces);
 01425                }
 01426                var pool = host.CreateRunspacePool(MaxRunspaces ?? host.Options.MaxSchedulerRunspaces);
 01427                var logger = Logger.ForContext<KestrunHost>();
 01428                host.Tasks = new KestrunTaskService(pool, logger);
 01429            }
 01430            else
 01431            {
 01432                Logger.Warning("KestrunTaskService already configured; skipping.");
 01433            }
 01434        });
 1435    }
 1436
 1437    /// <summary>
 1438    /// Adds MVC / API controllers to the application.
 1439    /// </summary>
 1440    /// <param name="cfg">The configuration options for MVC / API controllers.</param>
 1441    /// <returns>The current KestrunHost instance.</returns>
 1442    public KestrunHost AddControllers(Action<Microsoft.AspNetCore.Mvc.MvcOptions>? cfg = null)
 1443    {
 01444        return AddService(services =>
 01445        {
 01446            var builder = services.AddControllers();
 01447            if (cfg != null)
 01448            {
 01449                _ = builder.ConfigureApplicationPartManager(pm => { }); // customise if you wish
 01450            }
 01451        });
 1452    }
 1453
 1454    /// <summary>
 1455    /// Adds a PowerShell runtime to the application.
 1456    /// This middleware allows you to execute PowerShell scripts in response to HTTP requests.
 1457    /// </summary>
 1458    /// <param name="routePrefix">The route prefix to use for the PowerShell runtime.</param>
 1459    /// <returns>The current KestrunHost instance.</returns>
 1460    public KestrunHost AddPowerShellRuntime(PathString? routePrefix = null)
 1461    {
 11462        if (Logger.IsEnabled(LogEventLevel.Debug))
 1463        {
 11464            Logger.Debug("Adding PowerShell runtime with route prefix: {RoutePrefix}", routePrefix);
 1465        }
 1466
 11467        return Use(app =>
 11468        {
 11469            ArgumentNullException.ThrowIfNull(_runspacePool);
 11470            // ── mount PowerShell at the root ──
 11471            _ = app.UseLanguageRuntime(
 11472                ScriptLanguage.PowerShell,
 21473                b => b.UsePowerShellRunspace(_runspacePool));
 21474        });
 1475    }
 1476
 1477    /// <summary>
 1478    /// Adds a SignalR hub to the application at the specified path.
 1479    /// </summary>
 1480    /// <typeparam name="T">The type of the SignalR hub.</typeparam>
 1481    /// <param name="path">The path at which to map the SignalR hub.</param>
 1482    /// <returns>The current KestrunHost instance.</returns>
 1483    public KestrunHost AddSignalR<T>(string path) where T : Hub
 1484    {
 01485        return AddService(s =>
 01486        {
 01487            _ = s.AddSignalR().AddJsonProtocol(opts =>
 01488            {
 01489                // Avoid failures when payloads contain cycles; our sanitizer should prevent most, this is a safety net.
 01490                opts.PayloadSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreC
 01491            });
 01492            // Register IRealtimeBroadcaster as singleton if it's the KestrunHub
 01493            if (typeof(T) == typeof(SignalR.KestrunHub))
 01494            {
 01495                _ = s.AddSingleton<SignalR.IRealtimeBroadcaster, SignalR.RealtimeBroadcaster>();
 01496                _ = s.AddSingleton<SignalR.IConnectionTracker, SignalR.InMemoryConnectionTracker>();
 01497            }
 01498        })
 01499        .Use(app => ((IEndpointRouteBuilder)app).MapHub<T>(path));
 1500    }
 1501
 1502    /// <summary>
 1503    /// Adds the default SignalR hub (KestrunHub) to the application at the specified path.
 1504    /// </summary>
 1505    /// <param name="path">The path at which to map the SignalR hub.</param>
 1506    /// <returns></returns>
 01507    public KestrunHost AddSignalR(string path) => AddSignalR<SignalR.KestrunHub>(path);
 1508
 1509    /*
 1510        // ④ gRPC
 1511        public KestrunHost AddGrpc<TService>() where TService : class
 1512        {
 1513            return AddService(s => s.AddGrpc())
 1514                   .Use(app => app.MapGrpcService<TService>());
 1515        }
 1516    */
 1517
 1518    // Add as many tiny helpers as you wish:
 1519    // • AddAuthentication(jwt => { … })
 1520    // • AddSignalR()
 1521    // • AddHealthChecks()
 1522    // • AddGrpc()
 1523    // etc.
 1524
 1525    #endregion
 1526    #region Run/Start/Stop
 1527
 1528    /// <summary>
 1529    /// Runs the Kestrun web application, applying configuration and starting the server.
 1530    /// </summary>
 1531    public void Run()
 1532    {
 01533        if (Logger.IsEnabled(LogEventLevel.Debug))
 1534        {
 01535            Logger.Debug("Run() called");
 1536        }
 1537
 01538        EnableConfiguration();
 01539        StartTime = DateTime.UtcNow;
 01540        _app?.Run();
 01541    }
 1542
 1543    /// <summary>
 1544    /// Starts the Kestrun web application asynchronously.
 1545    /// </summary>
 1546    /// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
 1547    /// <returns>A task that represents the asynchronous start operation.</returns>
 1548    public async Task StartAsync(CancellationToken cancellationToken = default)
 1549    {
 171550        if (Logger.IsEnabled(LogEventLevel.Debug))
 1551        {
 11552            Logger.Debug("StartAsync() called");
 1553        }
 1554
 171555        EnableConfiguration();
 171556        if (_app != null)
 1557        {
 171558            StartTime = DateTime.UtcNow;
 171559            await _app.StartAsync(cancellationToken);
 1560        }
 171561    }
 1562
 1563    /// <summary>
 1564    /// Stops the Kestrun web application asynchronously.
 1565    /// </summary>
 1566    /// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
 1567    /// <returns>A task that represents the asynchronous stop operation.</returns>
 1568    public async Task StopAsync(CancellationToken cancellationToken = default)
 1569    {
 221570        if (Logger.IsEnabled(LogEventLevel.Debug))
 1571        {
 61572            Logger.Debug("StopAsync() called");
 1573        }
 1574
 221575        if (_app != null)
 1576        {
 1577            try
 1578            {
 1579                // Initiate graceful shutdown
 171580                await _app.StopAsync(cancellationToken);
 171581                StopTime = DateTime.UtcNow;
 171582            }
 01583            catch (Exception ex) when (ex.GetType().FullName == "System.Net.Quic.QuicException")
 1584            {
 1585                // QUIC exceptions can occur during shutdown, especially if the server is not using QUIC.
 1586                // We log this as a debug message to avoid cluttering the logs with expected exceptions.
 1587                // This is a workaround for
 1588
 01589                Logger.Debug("Ignored QUIC exception during shutdown: {Message}", ex.Message);
 01590            }
 1591        }
 221592    }
 1593
 1594    /// <summary>
 1595    /// Initiates a graceful shutdown of the Kestrun web application.
 1596    /// </summary>
 1597    public void Stop()
 1598    {
 11599        if (Interlocked.Exchange(ref _stopping, 1) == 1)
 1600        {
 01601            return; // already stopping
 1602        }
 11603        if (Logger.IsEnabled(LogEventLevel.Debug))
 1604        {
 11605            Logger.Debug("Stop() called");
 1606        }
 1607        // This initiates a graceful shutdown.
 11608        _app?.Lifetime.StopApplication();
 11609        StopTime = DateTime.UtcNow;
 11610    }
 1611
 1612    /// <summary>
 1613    /// Determines whether the Kestrun web application is currently running.
 1614    /// </summary>
 1615    /// <returns>True if the application is running; otherwise, false.</returns>
 1616    public bool IsRunning
 1617    {
 1618        get
 1619        {
 81620            var appField = typeof(KestrunHost)
 81621                .GetField("_app", BindingFlags.NonPublic | BindingFlags.Instance);
 1622
 81623            return appField?.GetValue(this) is WebApplication app && !app.Lifetime.ApplicationStopping.IsCancellationReq
 1624        }
 1625    }
 1626
 1627    #endregion
 1628
 1629    #region Runspace Pool Management
 1630
 1631    /// <summary>
 1632    /// Creates and returns a new <see cref="KestrunRunspacePoolManager"/> instance with the specified maximum number of
 1633    /// </summary>
 1634    /// <param name="maxRunspaces">The maximum number of runspaces to create. If not specified or zero, defaults to twic
 1635    /// <param name="userVariables">A dictionary of user-defined variables to inject into the runspace pool.</param>
 1636    /// <param name="userFunctions">A dictionary of user-defined functions to inject into the runspace pool.</param>
 1637    /// <param name="openApiClassesPath">The file path to the OpenAPI class definitions to inject into the runspace pool
 1638    /// <returns>A configured <see cref="KestrunRunspacePoolManager"/> instance.</returns>
 1639    public KestrunRunspacePoolManager CreateRunspacePool(int? maxRunspaces = 0, Dictionary<string, object>? userVariable
 1640    {
 651641        LogCreateRunspacePool(maxRunspaces);
 1642
 651643        var iss = BuildInitialSessionState(openApiClassesPath);
 651644        AddHostVariables(iss);
 651645        AddSharedVariables(iss);
 651646        AddUserVariables(iss, userVariables);
 651647        AddUserFunctions(iss, userFunctions);
 1648
 651649        var maxRs = ResolveMaxRunspaces(maxRunspaces);
 1650
 651651        Logger.Information("Creating runspace pool with max runspaces: {MaxRunspaces}", maxRs);
 651652        return new KestrunRunspacePoolManager(this, Options?.MinRunspaces ?? 1, maxRunspaces: maxRs, initialSessionState
 1653    }
 1654
 1655    private void LogCreateRunspacePool(int? maxRunspaces)
 1656    {
 651657        if (Logger.IsEnabled(LogEventLevel.Debug))
 1658        {
 461659            Logger.Debug("CreateRunspacePool() called: {@MaxRunspaces}", maxRunspaces);
 1660        }
 651661    }
 1662
 1663    private InitialSessionState BuildInitialSessionState(string? openApiClassesPath)
 1664    {
 651665        var iss = InitialSessionState.CreateDefault();
 1666
 651667        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
 1668        {
 1669            // On Windows, we can use the full .NET Framework modules
 01670            iss.ExecutionPolicy = ExecutionPolicy.Unrestricted;
 1671        }
 1672
 651673        ImportModulePaths(iss);
 651674        AddOpenApiStartupScript(iss, openApiClassesPath);
 1675
 651676        return iss;
 1677    }
 1678
 1679    private void ImportModulePaths(InitialSessionState iss)
 1680    {
 2601681        foreach (var path in _modulePaths)
 1682        {
 651683            iss.ImportPSModule([path]);
 1684        }
 651685    }
 1686
 1687    private void AddOpenApiStartupScript(InitialSessionState iss, string? openApiClassesPath)
 1688    {
 651689        if (string.IsNullOrWhiteSpace(openApiClassesPath))
 1690        {
 641691            return;
 1692        }
 1693
 11694        _ = iss.StartupScripts.Add(openApiClassesPath);
 11695        if (Logger.IsEnabled(LogEventLevel.Debug))
 1696        {
 11697            Logger.Debug("Configured OpenAPI class script at {ScriptPath}", openApiClassesPath);
 1698        }
 11699    }
 1700
 1701    private void AddHostVariables(InitialSessionState iss)
 1702    {
 651703        iss.Variables.Add(
 651704            new SessionStateVariableEntry(
 651705                "KrServer",
 651706                this,
 651707                "The Kestrun Server Host (KestrunHost) instance"
 651708            )
 651709        );
 651710    }
 1711
 1712    private void AddSharedVariables(InitialSessionState iss)
 1713    {
 1301714        foreach (var kvp in SharedState.Snapshot())
 1715        {
 01716            iss.Variables.Add(
 01717                new SessionStateVariableEntry(
 01718                    kvp.Key,
 01719                    kvp.Value,
 01720                    "Global variable"
 01721                )
 01722            );
 1723        }
 651724    }
 1725
 1726    private static void AddUserVariables(InitialSessionState iss, IReadOnlyDictionary<string, object>? userVariables)
 1727    {
 651728        if (userVariables is null)
 1729        {
 621730            return;
 1731        }
 1732
 141733        foreach (var kvp in userVariables)
 1734        {
 41735            if (kvp.Value is PSVariable psVar)
 1736            {
 11737                iss.Variables.Add(
 11738                    new SessionStateVariableEntry(
 11739                        kvp.Key,
 11740                        psVar.Value,
 11741                        psVar.Description ?? "User-defined variable"
 11742                    )
 11743                );
 11744                continue;
 1745            }
 1746
 31747            iss.Variables.Add(
 31748                new SessionStateVariableEntry(
 31749                    kvp.Key,
 31750                    kvp.Value,
 31751                    "User-defined variable"
 31752                )
 31753            );
 1754        }
 31755    }
 1756
 1757    private static void AddUserFunctions(InitialSessionState iss, IReadOnlyDictionary<string, string>? userFunctions)
 1758    {
 651759        if (userFunctions is null)
 1760        {
 621761            return;
 1762        }
 1763
 121764        foreach (var function in userFunctions)
 1765        {
 31766            var entry = new SessionStateFunctionEntry(
 31767                function.Key,
 31768                function.Value,
 31769                ScopedItemOptions.ReadOnly,
 31770                helpFile: null
 31771            );
 1772
 31773            iss.Commands.Add(entry);
 1774        }
 31775    }
 1776
 1777    private static int ResolveMaxRunspaces(int? maxRunspaces) =>
 651778        (maxRunspaces.HasValue && maxRunspaces.Value > 0)
 651779            ? maxRunspaces.Value
 651780            : Environment.ProcessorCount * 2;
 1781
 1782    #endregion
 1783
 1784    #region Disposable
 1785
 1786    /// <summary>
 1787    /// Releases all resources used by the <see cref="KestrunHost"/> instance.
 1788    /// </summary>
 1789    public void Dispose()
 1790    {
 1601791        if (Logger.IsEnabled(LogEventLevel.Debug))
 1792        {
 1551793            Logger.Debug("Dispose() called");
 1794        }
 1795
 1601796        _runspacePool?.Dispose();
 1601797        _runspacePool = null; // Clear the runspace pool reference
 1601798        IsConfigured = false; // Reset configuration state
 1601799        _app = null;
 1601800        _scheduler?.Dispose();
 1601801        (Logger as IDisposable)?.Dispose();
 1601802        GC.SuppressFinalize(this);
 1601803    }
 1804    #endregion
 1805
 1806    #region Script Validation
 1807
 1808    #endregion
 1809}

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

#LineLine coverage
 1using System.Reflection;
 2using Kestrun.Razor;
 3using Kestrun.Scripting;
 4using Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation;
 5using Microsoft.AspNetCore.Mvc.RazorPages;
 6using Microsoft.Extensions.FileProviders;
 7using Serilog.Events;
 8
 9namespace Kestrun.Hosting;
 10
 11/// <summary>
 12/// Provides extension methods for adding PowerShell and Razor Pages to a Kestrun
 13/// </summary>
 14public partial class KestrunHost
 15{
 16    /// <summary>
 17    /// Adds PowerShell Razor Pages to the application.
 18    /// This middleware allows you to serve Razor Pages using PowerShell scripts.
 19    /// </summary>
 20    /// <param name="rootPath">The root directory for the Razor Pages.</param>
 21    /// <param name="routePrefix">The route prefix to use for the PowerShell Razor Pages.</param>
 22    /// <param name="cfg">Configuration options for the Razor Pages.</param>
 23    /// <returns>The current KestrunHost instance.</returns>
 24    public KestrunHost AddPowerShellRazorPages(string? rootPath, PathString? routePrefix, RazorPagesOptions? cfg)
 25    {
 526        if (Logger.IsEnabled(LogEventLevel.Debug))
 27        {
 028            Logger.Debug("Adding PowerShell Razor Pages with route prefix: {RoutePrefix}, config: {@Config}", routePrefi
 29        }
 30
 531        return AddPowerShellRazorPages(rootPath, routePrefix, dest =>
 532            {
 233                if (cfg != null)
 534                {
 535                    // simple value properties are fine
 036                    dest.RootDirectory = cfg.RootDirectory;
 537
 538                    // copy conventions one‑by‑one (collection is read‑only)
 039                    foreach (var c in cfg.Conventions)
 540                    {
 041                        dest.Conventions.Add(c);
 542                    }
 543                }
 744            });
 45    }
 46
 47    /// <summary>
 48    /// Adds PowerShell Razor Pages to the application.
 49    /// This middleware allows you to serve Razor Pages using PowerShell scripts.
 50    /// </summary>
 51    /// <param name="routePrefix">The route prefix to use for the PowerShell Razor Pages.</param>
 52    /// <returns>The current KestrunHost instance.</returns>
 53    public KestrunHost AddPowerShellRazorPages(PathString? routePrefix) =>
 054        AddPowerShellRazorPages(rootPath: null, routePrefix: routePrefix, cfg: null as RazorPagesOptions);
 55
 56    /// <summary>
 57    /// Adds PowerShell Razor Pages to the application.
 58    /// </summary>
 59    /// <param name="rootPath">The root directory for the Razor Pages.</param>
 60    /// <param name="routePrefix">The route prefix to use for the PowerShell Razor Pages.</param>
 61    /// <returns>The current KestrunHost instance.</returns>
 62    public KestrunHost AddPowerShellRazorPages(string? rootPath, PathString? routePrefix) =>
 163            AddPowerShellRazorPages(rootPath: rootPath, routePrefix: routePrefix, cfg: null as RazorPagesOptions);
 64
 65    /// <summary>
 66    /// Adds PowerShell Razor Pages to the application with default configuration and no route prefix.
 67    /// </summary>
 68    /// <returns>The current KestrunHost instance.</returns>
 69    public KestrunHost AddPowerShellRazorPages() =>
 270        AddPowerShellRazorPages(rootPath: null, routePrefix: null, cfg: null as RazorPagesOptions);
 71
 72    /// <summary>
 73    /// Adds PowerShell Razor Pages to the application.
 74    /// </summary>
 75    /// <param name="rootPath">The root directory for the Razor Pages.</param>
 76    /// <returns>The current KestrunHost instance.</returns>
 77    public KestrunHost AddPowerShellRazorPages(string? rootPath) =>
 178        AddPowerShellRazorPages(rootPath: rootPath, routePrefix: null, cfg: null as RazorPagesOptions);
 79
 80    /// <summary>
 81    /// Adds PowerShell Razor Pages to the application.
 82    /// This middleware allows you to serve Razor Pages using PowerShell scripts.
 83    /// </summary>
 84    /// <param name="rootPath">The root directory for the Razor Pages.</param>
 85    /// <param name="routePrefix">The route prefix to use for the PowerShell Razor Pages.</param>
 86    /// <param name="cfg">Configuration options for the Razor Pages.</param>
 87    /// <returns>The current KestrunHost instance.</returns>
 88    public KestrunHost AddPowerShellRazorPages(string? rootPath, PathString? routePrefix, Action<RazorPagesOptions>? cfg
 89    {
 590        LogAddPowerShellRazorPages(routePrefix, cfg);
 91
 592        var env = Builder.Environment;
 593        var isDefaultPath = string.IsNullOrWhiteSpace(rootPath);
 594        var pagesRootPath = ResolvePagesRootPath(env.ContentRootPath, rootPath, isDefaultPath);
 595        var rootDirectory = ResolveRazorRootDirectory(env.ContentRootPath, pagesRootPath, isDefaultPath);
 96
 597        _ = AddService(services =>
 598        {
 299            LogAddPowerShellRazorPagesService(routePrefix);
 5100
 2101            var mvcBuilder = ConfigureRazorPages(services, rootDirectory, cfg);
 2102            _ = mvcBuilder.AddRazorRuntimeCompilation();
 5103
 2104            ConfigureRuntimeCompilationReferences(services, pagesRootPath);
 7105        });
 106
 5107        return Use(app =>
 5108        {
 2109            ArgumentNullException.ThrowIfNull(RunspacePool);
 2110            LogAddPowerShellRazorPagesMiddleware(routePrefix);
 5111
 2112            MapPowerShellRazorPages(app, RunspacePool, pagesRootPath, routePrefix);
 5113
 2114            LogAddPowerShellRazorPagesMiddlewareAdded(routePrefix);
 7115        });
 116    }
 117
 118    /// <summary>
 119    /// Logs that PowerShell Razor Pages are being added.
 120    /// </summary>
 121    /// <param name="routePrefix">Optional route prefix for mounting Razor Pages.</param>
 122    /// <param name="cfg">Optional Razor Pages configuration delegate.</param>
 123    private void LogAddPowerShellRazorPages(PathString? routePrefix, Action<RazorPagesOptions>? cfg)
 124    {
 5125        if (Logger.IsEnabled(LogEventLevel.Debug))
 126        {
 0127            Logger.Debug("Adding PowerShell Razor Pages with route prefix: {RoutePrefix}, config: {@Config}", routePrefi
 128        }
 5129    }
 130
 131    /// <summary>
 132    /// Logs that PowerShell Razor Pages services are being added.
 133    /// </summary>
 134    /// <param name="routePrefix">Optional route prefix for mounting Razor Pages.</param>
 135    private void LogAddPowerShellRazorPagesService(PathString? routePrefix)
 136    {
 2137        if (Logger.IsEnabled(LogEventLevel.Debug))
 138        {
 0139            Logger.Debug("Adding PowerShell Razor Pages to the service with route prefix: {RoutePrefix}", routePrefix);
 140        }
 2141    }
 142
 143    /// <summary>
 144    /// Logs that PowerShell Razor Pages middleware is being added.
 145    /// </summary>
 146    /// <param name="routePrefix">Optional route prefix for mounting Razor Pages.</param>
 147    private void LogAddPowerShellRazorPagesMiddleware(PathString? routePrefix)
 148    {
 2149        if (Logger.IsEnabled(LogEventLevel.Debug))
 150        {
 0151            Logger.Debug("Adding PowerShell Razor Pages middleware with route prefix: {RoutePrefix}", routePrefix);
 152        }
 2153    }
 154
 155    /// <summary>
 156    /// Logs that PowerShell Razor Pages middleware has been added.
 157    /// </summary>
 158    /// <param name="routePrefix">Optional route prefix for mounting Razor Pages.</param>
 159    private void LogAddPowerShellRazorPagesMiddlewareAdded(PathString? routePrefix)
 160    {
 2161        if (Logger.IsEnabled(LogEventLevel.Debug))
 162        {
 0163            Logger.Debug("PowerShell Razor Pages middleware added with route prefix: {RoutePrefix}", routePrefix);
 164        }
 2165    }
 166
 167    /// <summary>
 168    /// Resolves the filesystem path used as the Pages root.
 169    /// </summary>
 170    /// <param name="contentRootPath">The application content root path.</param>
 171    /// <param name="rootPath">Optional explicit Pages root path.</param>
 172    /// <param name="isDefaultPath">Whether <paramref name="rootPath"/> was not provided.</param>
 173    /// <returns>The resolved Pages root path.</returns>
 174    private string ResolvePagesRootPath(string contentRootPath, string? rootPath, bool isDefaultPath)
 175    {
 5176        return isDefaultPath
 5177            ? Path.Combine(contentRootPath, "Pages")
 5178            : rootPath!;
 179    }
 180
 181    /// <summary>
 182    /// Resolves the Razor Pages <see cref="RazorPagesOptions.RootDirectory"/> value.
 183    /// </summary>
 184    /// <param name="contentRootPath">The application content root path.</param>
 185    /// <param name="pagesRootPath">The resolved Pages root filesystem path.</param>
 186    /// <param name="isDefaultPath">Whether the Pages root is the default path.</param>
 187    /// <returns>The RootDirectory value to apply, or <c>null</c> if no override should be applied.</returns>
 188    private string? ResolveRazorRootDirectory(string contentRootPath, string pagesRootPath, bool isDefaultPath)
 189    {
 5190        if (isDefaultPath)
 191        {
 3192            return null;
 193        }
 194
 2195        var relative = Path.GetRelativePath(contentRootPath, pagesRootPath)
 2196            .Replace("\\", "/");
 2197        return "/" + relative;
 198    }
 199
 200    /// <summary>
 201    /// Configures Razor Pages and applies optional RootDirectory and user configuration.
 202    /// </summary>
 203    /// <param name="services">The service collection.</param>
 204    /// <param name="rootDirectory">Optional Razor Pages root directory (virtual path) to apply.</param>
 205    /// <param name="cfg">Optional user configuration delegate for Razor Pages options.</param>
 206    /// <returns>The MVC builder instance.</returns>
 207    private static IMvcBuilder ConfigureRazorPages(IServiceCollection services, string? rootDirectory, Action<RazorPages
 208    {
 2209        var mvcBuilder = services.AddRazorPages();
 210
 2211        if (!string.IsNullOrWhiteSpace(rootDirectory))
 212        {
 2213            _ = mvcBuilder.AddRazorPagesOptions(opts => opts.RootDirectory = rootDirectory);
 214        }
 215
 2216        if (cfg != null)
 217        {
 2218            _ = mvcBuilder.AddRazorPagesOptions(cfg);
 219        }
 220
 2221        return mvcBuilder;
 222    }
 223
 224    /// <summary>
 225    /// Configures runtime compilation reference paths and optional file watching for the Pages directory.
 226    /// </summary>
 227    /// <param name="services">The service collection.</param>
 228    /// <param name="pagesRootPath">The resolved Pages directory path.</param>
 229    private void ConfigureRuntimeCompilationReferences(IServiceCollection services, string pagesRootPath)
 230    {
 2231        _ = services.Configure<MvcRazorRuntimeCompilationOptions>(opts =>
 2232        {
 2233            AddLoadedAssemblyReferences(opts);
 2234            AddSharedFrameworkReferences(opts);
 2235            AddPagesFileProviderIfExists(opts, pagesRootPath);
 4236        });
 2237    }
 238
 239    /// <summary>
 240    /// Adds already-loaded managed assemblies as Roslyn reference paths.
 241    /// </summary>
 242    /// <param name="opts">Runtime compilation options to update.</param>
 243    private void AddLoadedAssemblyReferences(MvcRazorRuntimeCompilationOptions opts)
 244    {
 1074245        foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()
 711246            .Where(a => !a.IsDynamic && IsManaged(a.Location)))
 247        {
 535248            opts.AdditionalReferencePaths.Add(asm.Location);
 249        }
 2250    }
 251
 252    /// <summary>
 253    /// Adds managed DLLs from the shared framework directory as Roslyn reference paths.
 254    /// </summary>
 255    /// <param name="opts">Runtime compilation options to update.</param>
 256    private void AddSharedFrameworkReferences(MvcRazorRuntimeCompilationOptions opts)
 257    {
 2258        var coreDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!;
 676259        foreach (var dll in Directory.EnumerateFiles(coreDir, "*.dll").Where(IsManaged))
 260        {
 336261            opts.AdditionalReferencePaths.Add(dll);
 262        }
 2263    }
 264
 265    /// <summary>
 266    /// Adds a file provider for the Pages directory so Razor runtime compilation can watch changes.
 267    /// </summary>
 268    /// <param name="opts">Runtime compilation options to update.</param>
 269    /// <param name="pagesRootPath">The resolved Pages directory path.</param>
 270    private void AddPagesFileProviderIfExists(MvcRazorRuntimeCompilationOptions opts, string pagesRootPath)
 271    {
 2272        if (Directory.Exists(pagesRootPath))
 273        {
 2274            opts.FileProviders.Add(new PhysicalFileProvider(pagesRootPath));
 275        }
 2276    }
 277
 278    /// <summary>
 279    /// Maps PowerShell Razor Pages middleware either at the application root or under a route prefix.
 280    /// </summary>
 281    /// <param name="app">The application builder.</param>
 282    /// <param name="pool">The runspace pool manager for PowerShell execution.</param>
 283    /// <param name="pagesRootPath">The resolved Pages directory path.</param>
 284    /// <param name="routePrefix">Optional route prefix for mounting Razor Pages.</param>
 285    private void MapPowerShellRazorPages(IApplicationBuilder app, KestrunRunspacePoolManager pool, string pagesRootPath,
 286    {
 2287        if (routePrefix.HasValue)
 288        {
 0289            _ = app.Map(routePrefix.Value, branch =>
 0290            {
 0291                _ = branch.UsePowerShellRazorPages(pool, pagesRootPath);
 0292                _ = branch.UseRouting();
 0293                _ = branch.UseEndpoints(e => e.MapRazorPages());
 0294            });
 295
 0296            return;
 297        }
 298
 2299        _ = app.UsePowerShellRazorPages(pool, pagesRootPath);
 2300        _ = app.UseRouting();
 4301        _ = app.UseEndpoints(e => e.MapRazorPages());
 2302    }
 303
 304    /// <summary>
 305    /// Adds Razor Pages to the application.
 306    /// </summary>
 307    /// <param name="cfg">The configuration options for Razor Pages.</param>
 308    /// <returns>The current KestrunHost instance.</returns>
 309    public KestrunHost AddRazorPages(RazorPagesOptions? cfg)
 310    {
 1311        if (Logger.IsEnabled(LogEventLevel.Debug))
 312        {
 0313            Logger.Debug("Adding Razor Pages from source: {Source}", cfg);
 314        }
 315
 1316        if (cfg == null)
 317        {
 0318            return AddRazorPages(); // no config, use defaults
 319        }
 320
 1321        return AddRazorPages(dest =>
 1322            {
 1323                // simple value properties are fine
 1324                dest.RootDirectory = cfg.RootDirectory;
 1325
 1326                // copy conventions one‑by‑one (collection is read‑only)
 2327                foreach (var c in cfg.Conventions)
 1328                {
 0329                    dest.Conventions.Add(c);
 1330                }
 2331            });
 332    }
 333
 334    /// <summary>
 335    /// Adds Razor Pages to the application.
 336    /// This overload allows you to specify configuration options.
 337    /// If you need to configure Razor Pages options, use the other overload.
 338    /// </summary>
 339    /// <param name="cfg">The configuration options for Razor Pages.</param>
 340    /// <returns>The current KestrunHost instance.</returns>
 341    public KestrunHost AddRazorPages(Action<RazorPagesOptions>? cfg = null)
 342    {
 3343        if (Logger.IsEnabled(LogEventLevel.Debug))
 344        {
 0345            Logger.Debug("Adding Razor Pages with configuration: {Config}", cfg);
 346        }
 347
 3348        return AddService(services =>
 3349        {
 3350            var mvc = services.AddRazorPages();         // returns IMvcBuilder
 3351
 3352            if (cfg != null)
 3353            {
 2354                _ = mvc.AddRazorPagesOptions(cfg);          // ← the correct extension
 3355            }
 3356            //  —OR—
 3357            // services.Configure(cfg);                 // also works
 3358        })
 6359         .Use(app => ((IEndpointRouteBuilder)app).MapRazorPages());// optional: automatically map Razor endpoints after 
 360    }
 361
 362    // helper: true  ⇢ file contains managed metadata
 363    private bool IsManaged(string path)
 364    {
 1908365        try { _ = AssemblyName.GetAssemblyName(path); return true; }
 332366        catch { return false; }          // native ⇒ BadImageFormatException
 1037367    }
 368}

Methods/Properties

get_Builder()
get_App()
get_ApplicationName()
get_Options()
.ctor(System.String,Serilog.ILogger,System.String,System.String[],System.String[],System.Boolean)
get_IsConfigured()
get_StartTime()
get_StopTime()
get_Uptime()
get_RunspacePool()
get_FeatureQueue()
get_HealthProbes()
get_KestrunRoot()
get_ModulePaths()
get_SharedState()
get_Logger()
get_Scheduler()
set_Scheduler(Kestrun.Scheduling.SchedulerService)
get_Tasks()
set_Tasks(Kestrun.Tasks.KestrunTaskService)
get_RouteGroupStack()
get_RegisteredRoutes()
get_RegisteredAuthentications()
get_DefaultCacheControl()
get_PowershellMiddlewareEnabled()
get_DefaultHost()
get_DefinedCorsPolicyNames()
get_CorsPolicyDefined()
get_StatusCodeOptions()
set_StatusCodeOptions(Kestrun.Hosting.Options.StatusCodeOptions)
get_ExceptionOptions()
set_ExceptionOptions(Kestrun.Hosting.Options.ExceptionOptions)
get_ForwardedHeaderOptions()
set_ForwardedHeaderOptions(Microsoft.AspNetCore.Builder.ForwardedHeadersOptions)
get_AntiforgeryOptions()
get_OpenApiDocumentDescriptor()
.ctor(System.String,System.String,System.String[])
.ctor(System.String,Serilog.ILogger,System.Boolean)
CreateWebAppOptions()
GetOrCreateOpenApiDocument(System.String)
LogConstructorArgs(System.String,System.Boolean,System.String,System.Int32)
SetWorkingDirectoryIfNeeded(System.String)
GetSafeContentRootPath(System.String)
GetSafeCurrentDirectory()
AddKestrunModulePathIfMissing(System.String[])
InitializeOptions(System.String)
AddUserModulePaths(System.String[])
AddProbe(Kestrun.Health.IProbe)
AddProbe(System.String,System.String[],System.Func`2<System.Threading.CancellationToken,System.Threading.Tasks.Task`1<Kestrun.Health.ProbeResult>>)
AddProbe(System.String,System.String[],System.String,System.Nullable`1<Kestrun.Scripting.ScriptLanguage>,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>,System.String[],System.Reflection.Assembly[])
GetHealthProbesSnapshot()
RegisterProbeInternal(Kestrun.Health.IProbe)
ConfigureListener(System.Int32,System.Net.IPAddress,System.Security.Cryptography.X509Certificates.X509Certificate2,Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols,System.Boolean)
ConfigureListener(System.Int32,System.Net.IPAddress,System.Boolean)
ConfigureListener(System.Int32,System.Boolean)
ConfigureListener(System.String,System.Int32,System.Security.Cryptography.X509Certificates.X509Certificate2,Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols,System.Boolean,System.Net.Sockets.AddressFamily[])
ConfigureListener(System.Uri,System.Security.Cryptography.X509Certificates.X509Certificate2,System.Nullable`1<Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols>,System.Boolean,System.Net.Sockets.AddressFamily[])
ValidateConfiguration()
InitializeRunspacePool(System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.String>,System.String)
ConfigureKestrelBase()
ConfigureNamedPipes()
ConfigureHttpsAdapter(Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions)
BindListeners(Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions)
LogConfiguredEndpoints()
HandleConfigurationError(System.Exception)
EnableConfiguration(System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.String>)
RegisterDefaultHealthProbes()
Build()
ValidateBuilderState()
ApplyQueuedServices()
BuildWebApplication()
ConfigureBuiltInMiddleware()
ConfigureRouting()
ConfigureCors()
ConfigureExceptionHandling()
ConfigureForwardedHeaders()
ConfigureStatusCodePages()
ConfigurePowerShellRuntime()
LogApplicationInfo()
LogPagesDirectory()
ApplyQueuedMiddleware()
ApplyFeatures()
IsServiceRegistered(System.Type)
IsServiceRegistered()
AddService(System.Action`1<Microsoft.Extensions.DependencyInjection.IServiceCollection>)
Use(System.Action`1<Microsoft.AspNetCore.Builder.IApplicationBuilder>)
AddFeature(System.Action`1<Kestrun.Hosting.KestrunHost>)
AddScheduling(System.Nullable`1<System.Int32>)
AddTasks(System.Nullable`1<System.Int32>)
AddControllers(System.Action`1<Microsoft.AspNetCore.Mvc.MvcOptions>)
AddPowerShellRuntime(System.Nullable`1<Microsoft.AspNetCore.Http.PathString>)
AddSignalR(System.String)
Run()
StartAsync()
StopAsync()
Stop()
get_IsRunning()
CreateRunspacePool(System.Nullable`1<System.Int32>,System.Collections.Generic.Dictionary`2<System.String,System.Object>,System.Collections.Generic.Dictionary`2<System.String,System.String>,System.String)
LogCreateRunspacePool(System.Nullable`1<System.Int32>)
BuildInitialSessionState(System.String)
ImportModulePaths(System.Management.Automation.Runspaces.InitialSessionState)
AddOpenApiStartupScript(System.Management.Automation.Runspaces.InitialSessionState,System.String)
AddHostVariables(System.Management.Automation.Runspaces.InitialSessionState)
AddSharedVariables(System.Management.Automation.Runspaces.InitialSessionState)
AddUserVariables(System.Management.Automation.Runspaces.InitialSessionState,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>)
AddUserFunctions(System.Management.Automation.Runspaces.InitialSessionState,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>)
ResolveMaxRunspaces(System.Nullable`1<System.Int32>)
Dispose()
AddPowerShellRazorPages(System.String,System.Nullable`1<Microsoft.AspNetCore.Http.PathString>,Microsoft.AspNetCore.Mvc.RazorPages.RazorPagesOptions)
AddPowerShellRazorPages(System.Nullable`1<Microsoft.AspNetCore.Http.PathString>)
AddPowerShellRazorPages(System.String,System.Nullable`1<Microsoft.AspNetCore.Http.PathString>)
AddPowerShellRazorPages()
AddPowerShellRazorPages(System.String)
AddPowerShellRazorPages(System.String,System.Nullable`1<Microsoft.AspNetCore.Http.PathString>,System.Action`1<Microsoft.AspNetCore.Mvc.RazorPages.RazorPagesOptions>)
LogAddPowerShellRazorPages(System.Nullable`1<Microsoft.AspNetCore.Http.PathString>,System.Action`1<Microsoft.AspNetCore.Mvc.RazorPages.RazorPagesOptions>)
LogAddPowerShellRazorPagesService(System.Nullable`1<Microsoft.AspNetCore.Http.PathString>)
LogAddPowerShellRazorPagesMiddleware(System.Nullable`1<Microsoft.AspNetCore.Http.PathString>)
LogAddPowerShellRazorPagesMiddlewareAdded(System.Nullable`1<Microsoft.AspNetCore.Http.PathString>)
ResolvePagesRootPath(System.String,System.String,System.Boolean)
ResolveRazorRootDirectory(System.String,System.String,System.Boolean)
ConfigureRazorPages(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.Action`1<Microsoft.AspNetCore.Mvc.RazorPages.RazorPagesOptions>)
ConfigureRuntimeCompilationReferences(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String)
AddLoadedAssemblyReferences(Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.MvcRazorRuntimeCompilationOptions)
AddSharedFrameworkReferences(Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.MvcRazorRuntimeCompilationOptions)
AddPagesFileProviderIfExists(Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.MvcRazorRuntimeCompilationOptions,System.String)
MapPowerShellRazorPages(Microsoft.AspNetCore.Builder.IApplicationBuilder,Kestrun.Scripting.KestrunRunspacePoolManager,System.String,System.Nullable`1<Microsoft.AspNetCore.Http.PathString>)
AddRazorPages(Microsoft.AspNetCore.Mvc.RazorPages.RazorPagesOptions)
AddRazorPages(System.Action`1<Microsoft.AspNetCore.Mvc.RazorPages.RazorPagesOptions>)
IsManaged(System.String)