< Summary - Kestrun — Combined Coverage

Line coverage
57%
Covered lines: 828
Uncovered lines: 613
Coverable lines: 1441
Total lines: 3592
Line coverage: 57.4%
Branch coverage
50%
Covered branches: 298
Total branches: 588
Branch coverage: 50.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 09/08/2025 - 20:34:03 Line coverage: 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@0d738bf294e6281b936d031e1979d928007495ff12/21/2025 - 19:25:34 Line coverage: 70.5% (545/773) Branch coverage: 61.1% (208/340) Total lines: 2189 Tag: Kestrun/Kestrun@63eee3e6ff7662a7eb5bb3603d667daccb809f2d12/23/2025 - 19:23:04 Line coverage: 69.5% (538/773) Branch coverage: 59.1% (201/340) Total lines: 2189 Tag: Kestrun/Kestrun@d062f281460e6c123c372aef61f8d957bbb6c90112/25/2025 - 19:20:44 Line coverage: 70.5% (545/773) Branch coverage: 61.1% (208/340) Total lines: 2189 Tag: Kestrun/Kestrun@5251f12f253e29f8a1dfb77edc2ef50b90a4f26f12/26/2025 - 04:28:42 Line coverage: 69.5% (538/773) Branch coverage: 59.1% (201/340) Total lines: 2189 Tag: Kestrun/Kestrun@078efbc0494329762e193e7b43b6ce82e494276412/26/2025 - 18:43:06 Line coverage: 70.4% (545/774) Branch coverage: 61.1% (208/340) Total lines: 2193 Tag: Kestrun/Kestrun@66a9a3a4461391825b9a1ffc8190f76adb1bb67f12/28/2025 - 21:05:19 Line coverage: 69.5% (538/774) Branch coverage: 59.1% (201/340) Total lines: 2193 Tag: Kestrun/Kestrun@1f86b77fc2bddfa8444a4bce466be6fec80b6db701/02/2026 - 00:16:25 Line coverage: 67% (539/804) Branch coverage: 56.7% (201/354) Total lines: 2239 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/02/2026 - 21:56:10 Line coverage: 67.9% (546/804) Branch coverage: 58.7% (208/354) Total lines: 2239 Tag: Kestrun/Kestrun@f60326065ebb24cf70b241e459b37baf142e6ed601/08/2026 - 02:20:28 Line coverage: 67.5% (573/848) Branch coverage: 56.6% (221/390) Total lines: 2374 Tag: Kestrun/Kestrun@4bc17b7e465c315de6386907c417e44fcb0fd3eb01/08/2026 - 08:19:25 Line coverage: 62.4% (722/1156) Branch coverage: 54.6% (273/500) Total lines: 2945 Tag: Kestrun/Kestrun@6ab94ca7560634c2ac58b36c2b98e2a9b1bf305d01/09/2026 - 02:57:32 Line coverage: 63% (729/1156) Branch coverage: 56% (280/500) Total lines: 2945 Tag: Kestrun/Kestrun@e3a5419e6c64edac795b8d501a4f334796c0913901/11/2026 - 19:55:44 Line coverage: 62.4% (722/1156) Branch coverage: 54.6% (273/500) Total lines: 2945 Tag: Kestrun/Kestrun@53c97a4806941d5aa8d4dcc6779071adf1ae537601/12/2026 - 18:03:06 Line coverage: 57.7% (825/1428) Branch coverage: 50.8% (298/586) Total lines: 3536 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/12/2026 - 22:21:01 Line coverage: 58.2% (832/1428) Branch coverage: 52% (305/586) Total lines: 3536 Tag: Kestrun/Kestrun@91f4d7da7b838286aa6f574ec486cbb057167d6401/14/2026 - 07:55:07 Line coverage: 57.7% (825/1428) Branch coverage: 50.8% (298/586) Total lines: 3536 Tag: Kestrun/Kestrun@13bd81d8920e7e63e39aafdd188e7d766641ad3501/14/2026 - 21:08:55 Line coverage: 58.2% (832/1428) Branch coverage: 52% (305/586) Total lines: 3536 Tag: Kestrun/Kestrun@b584423d949a5193ed8cd45cf9df490f06d1c54501/16/2026 - 03:52:31 Line coverage: 57.7% (825/1428) Branch coverage: 50.8% (298/586) Total lines: 3536 Tag: Kestrun/Kestrun@0077556dd757d1b7434cf700cd5c7be05cea351401/17/2026 - 04:33:35 Line coverage: 57.7% (828/1434) Branch coverage: 50.8% (298/586) Total lines: 3542 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba01/17/2026 - 18:18:02 Line coverage: 58.2% (835/1434) Branch coverage: 52% (305/586) Total lines: 3542 Tag: Kestrun/Kestrun@8dd16f7908c0e15b594d16bb49be0240e2c7c01801/18/2026 - 06:40:41 Line coverage: 57.7% (828/1435) Branch coverage: 50.8% (298/586) Total lines: 3549 Tag: Kestrun/Kestrun@99e92690d0fd95f6f4896f3410d2c024350a979401/19/2026 - 18:00:47 Line coverage: 58.1% (835/1435) Branch coverage: 52% (305/586) Total lines: 3549 Tag: Kestrun/Kestrun@4766238fbb08ddf2e8b6acb61058217338a9c28101/19/2026 - 18:47:02 Line coverage: 57.7% (828/1435) Branch coverage: 50.8% (298/586) Total lines: 3549 Tag: Kestrun/Kestrun@716db6917075bf04d6f8ae45a1bad48ca5cfacfe01/21/2026 - 17:07:46 Line coverage: 58.1% (835/1435) Branch coverage: 52% (305/586) Total lines: 3549 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a3601/24/2026 - 19:35:59 Line coverage: 57.4% (828/1441) Branch coverage: 50.6% (298/588) Total lines: 3592 Tag: Kestrun/Kestrun@f59dcba478ea75f69584d696e5f1fb1cfa40aa5102/01/2026 - 00:21:41 Line coverage: 57.9% (835/1441) Branch coverage: 51.8% (305/588) Total lines: 3592 Tag: Kestrun/Kestrun@7cc5472da9e24346545f7201f994930135f8e6a502/03/2026 - 18:01:44 Line coverage: 57.4% (828/1441) Branch coverage: 50.6% (298/588) Total lines: 3592 Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe 09/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@0d738bf294e6281b936d031e1979d928007495ff12/21/2025 - 19:25:34 Line coverage: 70.5% (545/773) Branch coverage: 61.1% (208/340) Total lines: 2189 Tag: Kestrun/Kestrun@63eee3e6ff7662a7eb5bb3603d667daccb809f2d12/23/2025 - 19:23:04 Line coverage: 69.5% (538/773) Branch coverage: 59.1% (201/340) Total lines: 2189 Tag: Kestrun/Kestrun@d062f281460e6c123c372aef61f8d957bbb6c90112/25/2025 - 19:20:44 Line coverage: 70.5% (545/773) Branch coverage: 61.1% (208/340) Total lines: 2189 Tag: Kestrun/Kestrun@5251f12f253e29f8a1dfb77edc2ef50b90a4f26f12/26/2025 - 04:28:42 Line coverage: 69.5% (538/773) Branch coverage: 59.1% (201/340) Total lines: 2189 Tag: Kestrun/Kestrun@078efbc0494329762e193e7b43b6ce82e494276412/26/2025 - 18:43:06 Line coverage: 70.4% (545/774) Branch coverage: 61.1% (208/340) Total lines: 2193 Tag: Kestrun/Kestrun@66a9a3a4461391825b9a1ffc8190f76adb1bb67f12/28/2025 - 21:05:19 Line coverage: 69.5% (538/774) Branch coverage: 59.1% (201/340) Total lines: 2193 Tag: Kestrun/Kestrun@1f86b77fc2bddfa8444a4bce466be6fec80b6db701/02/2026 - 00:16:25 Line coverage: 67% (539/804) Branch coverage: 56.7% (201/354) Total lines: 2239 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/02/2026 - 21:56:10 Line coverage: 67.9% (546/804) Branch coverage: 58.7% (208/354) Total lines: 2239 Tag: Kestrun/Kestrun@f60326065ebb24cf70b241e459b37baf142e6ed601/08/2026 - 02:20:28 Line coverage: 67.5% (573/848) Branch coverage: 56.6% (221/390) Total lines: 2374 Tag: Kestrun/Kestrun@4bc17b7e465c315de6386907c417e44fcb0fd3eb01/08/2026 - 08:19:25 Line coverage: 62.4% (722/1156) Branch coverage: 54.6% (273/500) Total lines: 2945 Tag: Kestrun/Kestrun@6ab94ca7560634c2ac58b36c2b98e2a9b1bf305d01/09/2026 - 02:57:32 Line coverage: 63% (729/1156) Branch coverage: 56% (280/500) Total lines: 2945 Tag: Kestrun/Kestrun@e3a5419e6c64edac795b8d501a4f334796c0913901/11/2026 - 19:55:44 Line coverage: 62.4% (722/1156) Branch coverage: 54.6% (273/500) Total lines: 2945 Tag: Kestrun/Kestrun@53c97a4806941d5aa8d4dcc6779071adf1ae537601/12/2026 - 18:03:06 Line coverage: 57.7% (825/1428) Branch coverage: 50.8% (298/586) Total lines: 3536 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/12/2026 - 22:21:01 Line coverage: 58.2% (832/1428) Branch coverage: 52% (305/586) Total lines: 3536 Tag: Kestrun/Kestrun@91f4d7da7b838286aa6f574ec486cbb057167d6401/14/2026 - 07:55:07 Line coverage: 57.7% (825/1428) Branch coverage: 50.8% (298/586) Total lines: 3536 Tag: Kestrun/Kestrun@13bd81d8920e7e63e39aafdd188e7d766641ad3501/14/2026 - 21:08:55 Line coverage: 58.2% (832/1428) Branch coverage: 52% (305/586) Total lines: 3536 Tag: Kestrun/Kestrun@b584423d949a5193ed8cd45cf9df490f06d1c54501/16/2026 - 03:52:31 Line coverage: 57.7% (825/1428) Branch coverage: 50.8% (298/586) Total lines: 3536 Tag: Kestrun/Kestrun@0077556dd757d1b7434cf700cd5c7be05cea351401/17/2026 - 04:33:35 Line coverage: 57.7% (828/1434) Branch coverage: 50.8% (298/586) Total lines: 3542 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba01/17/2026 - 18:18:02 Line coverage: 58.2% (835/1434) Branch coverage: 52% (305/586) Total lines: 3542 Tag: Kestrun/Kestrun@8dd16f7908c0e15b594d16bb49be0240e2c7c01801/18/2026 - 06:40:41 Line coverage: 57.7% (828/1435) Branch coverage: 50.8% (298/586) Total lines: 3549 Tag: Kestrun/Kestrun@99e92690d0fd95f6f4896f3410d2c024350a979401/19/2026 - 18:00:47 Line coverage: 58.1% (835/1435) Branch coverage: 52% (305/586) Total lines: 3549 Tag: Kestrun/Kestrun@4766238fbb08ddf2e8b6acb61058217338a9c28101/19/2026 - 18:47:02 Line coverage: 57.7% (828/1435) Branch coverage: 50.8% (298/586) Total lines: 3549 Tag: Kestrun/Kestrun@716db6917075bf04d6f8ae45a1bad48ca5cfacfe01/21/2026 - 17:07:46 Line coverage: 58.1% (835/1435) Branch coverage: 52% (305/586) Total lines: 3549 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a3601/24/2026 - 19:35:59 Line coverage: 57.4% (828/1441) Branch coverage: 50.6% (298/588) Total lines: 3592 Tag: Kestrun/Kestrun@f59dcba478ea75f69584d696e5f1fb1cfa40aa5102/01/2026 - 00:21:41 Line coverage: 57.9% (835/1441) Branch coverage: 51.8% (305/588) Total lines: 3592 Tag: Kestrun/Kestrun@7cc5472da9e24346545f7201f994930135f8e6a502/03/2026 - 18:01:44 Line coverage: 57.4% (828/1441) Branch coverage: 50.6% (298/588) Total lines: 3592 Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: AddHealthEndpoint(...)75%121294.11%
File 1: AddHealthEndpoint(...)100%11100%
File 1: ExtractTags(...)100%1414100%
File 1: CopyHealthEndpointOptions(...)100%1616100%
File 1: DetermineStatusCode(...)100%66100%
File 1: MapHealthEndpointImmediate(...)0%2040%
File 2: AddLocalization(...)0%620%
File 2: AddLocalization(...)100%210%
File 3: AddPowerShellRazorPages(...)33.33%7675%
File 3: AddPowerShellRazorPages(...)100%210%
File 3: AddPowerShellRazorPages(...)100%11100%
File 3: AddPowerShellRazorPages()100%11100%
File 3: AddPowerShellRazorPages(...)100%11100%
File 3: AddPowerShellRazorPages(...)100%11100%
File 3: LogAddPowerShellRazorPages(...)50%2266.66%
File 3: LogAddPowerShellRazorPagesService(...)50%2266.66%
File 3: LogAddPowerShellRazorPagesMiddleware(...)50%2266.66%
File 3: LogAddPowerShellRazorPagesMiddlewareAdded(...)50%2266.66%
File 3: ResolvePagesRootPath(...)100%22100%
File 3: ResolveRazorRootDirectory(...)100%22100%
File 3: ConfigureRazorPages(...)100%44100%
File 3: ConfigureRuntimeCompilationReferences(...)100%11100%
File 3: AddLoadedAssemblyReferences(...)100%22100%
File 3: AddSharedFrameworkReferences(...)100%22100%
File 3: AddPagesFileProviderIfExists(...)100%22100%
File 3: MapPowerShellRazorPages(...)50%3241.66%
File 3: AddRazorPages(...)50%6680%
File 3: AddRazorPages(...)75%4492.85%
File 3: IsManaged(...)100%11100%
File 4: AddSseBroadcast(...)100%44100%
File 4: AddSseTag(...)100%22100%
File 4: RegisterSseBroadcastRouteForOpenApi(...)68.75%1616100%
File 4: GetSseConnectedClientCount()0%4260%
File 4: BroadcastSseEventAsync()0%4260%
File 4: HandleSseBroadcastConnectAsync()0%620%
File 4: PumpSseAsync()0%7280%
File 4: WritePayloadAsync()100%210%
File 5: AddDefaultFiles(...)75%4475%
File 5: AddDefaultFiles(...)50%22100%
File 5: AddDirectoryBrowser(...)100%210%
File 5: AddFavicon(...)100%11100%
File 5: CopyStaticFileOptions(...)0%2040%
File 5: CopyDefaultFilesOptions(...)0%4260%
File 5: AddFileServer(...)16.66%1351828.88%
File 5: AddFileServer(...)50%4487.5%
File 5: AddStaticFiles(...)50%4487.5%
File 5: AddStaticFiles(...)18.75%921633.33%
File 6: .cctor()100%11100%
File 6: get_Builder()100%11100%
File 6: get_App()100%22100%
File 6: get_ApplicationName()100%22100%
File 6: get_Options()100%11100%
File 6: .ctor(...)75%4484.37%
File 6: get_IsConfigured()100%11100%
File 6: get_StartTime()100%11100%
File 6: get_StopTime()100%11100%
File 6: get_Uptime()0%4260%
File 6: get_RunspacePool()50%22100%
File 6: get_FeatureQueue()100%11100%
File 6: get_HealthProbes()100%11100%
File 6: get_KestrunRoot()100%11100%
File 6: get_ModulePaths()100%210%
File 6: get_SharedState()100%11100%
File 6: get_Logger()100%11100%
File 6: get_Scheduler()50%22100%
File 6: set_Scheduler(...)100%11100%
File 6: get_Tasks()0%620%
File 6: set_Tasks(...)100%210%
File 6: get_RouteGroupStack()100%11100%
File 6: get_RegisteredRoutes()100%11100%
File 6: get_RegisteredAuthentications()100%11100%
File 6: get_DefaultCacheControl()100%11100%
File 6: get_PowershellMiddlewareEnabled()100%11100%
File 6: get_LocalizationStore()100%210%
File 6: get_DefaultHost()100%11100%
File 6: get_DefinedCorsPolicyNames()100%11100%
File 6: get_CorsPolicyDefined()100%11100%
File 6: get_ComponentAnnotations()100%11100%
File 6: get_StatusCodeOptions()100%11100%
File 6: set_StatusCodeOptions(...)0%620%
File 6: get_ExceptionOptions()100%11100%
File 6: set_ExceptionOptions(...)50%2275%
File 6: get_ForwardedHeaderOptions()100%11100%
File 6: set_ForwardedHeaderOptions(...)100%22100%
File 6: get_AntiforgeryOptions()100%210%
File 6: get_OpenApiDocumentDescriptor()100%11100%
File 6: get_OpenApiDocumentIds()100%210%
File 6: get_DefaultOpenApiDocumentDescriptor()100%210%
File 6: .ctor(...)100%11100%
File 6: .ctor(...)100%210%
File 6: CreateWebAppOptions()100%22100%
File 6: GetOrCreateOpenApiDocument(...)83.33%6687.5%
File 6: GetOrCreateOpenApiDocument(...)100%22100%
File 6: LogConstructorArgs(...)100%22100%
File 6: SetWorkingDirectoryIfNeeded(...)100%44100%
File 6: GetSafeContentRootPath(...)75%44100%
File 6: GetSafeCurrentDirectory()100%11100%
File 6: AddKestrunModulePathIfMissing(...)75%8883.33%
File 6: InitializeOptions(...)100%22100%
File 6: AddUserModulePaths(...)70%111077.77%
File 6: AddProbe(...)100%210%
File 6: AddProbe(...)100%210%
File 6: AddProbe(...)0%2040%
File 6: GetHealthProbesSnapshot()100%210%
File 6: RegisterProbeInternal(...)50%2277.77%
File 6: AddCallbacksAutomation(...)0%210140%
File 6: ConfigureListener(...)90%101094.11%
File 6: ConfigureListener(...)100%11100%
File 6: ConfigureListener(...)100%11100%
File 6: ConfigureListener(...)0%272160%
File 6: ConfigureListener(...)0%110100%
File 6: ValidateConfiguration()100%66100%
File 6: InitializeRunspacePool(...)50%4483.33%
File 6: ConfigureKestrelBase()100%11100%
File 6: ConfigureNamedPipes()75%9433.33%
File 6: ConfigureHttpsAdapter(...)50%5213.33%
File 6: BindListeners(...)68.75%231669.56%
File 6: LogConfiguredEndpoints()25%5457.14%
File 6: HandleConfigurationError(...)100%11100%
File 6: EnableConfiguration(...)90%101089.65%
File 6: ApplyUserVarsToState(...)100%44100%
File 6: ExportOpenApiClasses(...)75%4483.33%
File 6: RegisterDefaultHealthProbes()50%2266.66%
File 6: Build()100%11100%
File 6: ValidateBuilderState()50%2266.66%
File 6: ApplyQueuedServices()100%22100%
File 6: BuildWebApplication()100%1172%
File 6: ConfigureBuiltInMiddleware()100%11100%
File 6: ConfigureRouting()100%44100%
File 6: ConfigureCors()33.33%19628.57%
File 6: ConfigureExceptionHandling()75%9877.77%
File 6: ConfigureForwardedHeaders()66.66%7671.42%
File 6: ConfigureStatusCodePages()33.33%19628.57%
File 6: ConfigurePowerShellRuntime()25%45816.66%
File 6: LogApplicationInfo()100%11100%
File 6: LogPagesDirectory()75%4485.71%
File 6: ApplyQueuedMiddleware()100%22100%
File 6: ApplyFeatures()100%22100%
File 6: IsServiceRegistered(...)50%44100%
File 6: IsServiceRegistered()100%210%
File 6: AddService(...)100%11100%
File 6: Use(...)100%11100%
File 6: AddFeature(...)100%11100%
File 6: AddScheduling(...)100%1212100%
File 6: AddTasks(...)0%210140%
File 6: AddControllers(...)0%620%
File 6: AddPowerShellRuntime(...)100%22100%
File 6: AddRealTimeTag(...)100%22100%
File 6: AddSignalRTag(...)0%620%
File 6: GetSignalRNegotiatePath(...)0%620%
File 6: CreateNativeRouteOptions(...)100%210%
File 6: RegisterRoute(...)100%210%
File 6: EnsureDefaultSignalRTags(...)0%4260%
File 6: CreateSignalRHubResponses()100%210%
File 6: CreateSignalRNegotiateResponses()100%210%
File 6: CreateSignalRExtensions(...)100%210%
File 6: TryAddSignalRHubOpenApiMetadata(...)0%156120%
File 6: TryAddSignalRNegotiateOpenApiMetadata(...)0%4260%
File 6: ConfigureSignalRServices(...)0%4260%
File 6: MapSignalRHub(...)100%210%
File 6: AddSignalR(...)0%7280%
File 6: Run()0%2040%
File 6: StartAsync()100%44100%
File 6: StopAsync()100%4470%
File 6: Stop()66.66%6685.71%
File 6: get_IsRunning()75%44100%
File 6: CreateRunspacePool(...)50%22100%
File 6: LogCreateRunspacePool(...)100%22100%
File 6: BuildInitialSessionState(...)50%2283.33%
File 6: ImportModulePaths(...)100%22100%
File 6: AddOpenApiStartupScript(...)100%44100%
File 6: AddHostVariables(...)100%11100%
File 6: AddSharedVariables(...)100%22100%
File 6: AddUserVariables(...)87.5%88100%
File 6: UnwrapKestrunVariableValue(...)20%401033.33%
File 6: UnwrapPsObject(...)50%22100%
File 6: IsKestrunVariableMarkerEnabled(...)0%4260%
File 6: TryGetDictionaryValueIgnoreCase(...)0%7280%
File 6: AddUserFunctions(...)100%44100%
File 6: ResolveMaxRunspaces(...)100%44100%
File 6: Dispose()75%88100%

File(s)

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

#LineLine coverage
 1using Kestrun.Health;
 2using Kestrun.Hosting.Options;
 3using Kestrun.Models;
 4using Kestrun.Scripting;
 5using Kestrun.Utilities;
 6using Microsoft.Net.Http.Headers;
 7
 8namespace Kestrun.Hosting;
 9
 10/// <summary>
 11/// Adds health-check specific helpers to <see cref="KestrunHost"/>.
 12/// </summary>
 13public partial class KestrunHost
 14{
 15    /// <summary>
 16    /// Registers a GET endpoint (default <c>/health</c>) that aggregates the state of all registered probes.
 17    /// </summary>
 18    /// <param name="configure">Optional action to mutate the default endpoint options.</param>
 19    /// <returns>The <see cref="KestrunHost"/> instance for fluent chaining.</returns>
 20    public KestrunHost AddHealthEndpoint(Action<HealthEndpointOptions>? configure = null)
 21    {
 522        var merged = Options.Health?.Clone() ?? new HealthEndpointOptions();
 523        configure?.Invoke(merged);
 24
 525        var mapOptions = new MapRouteOptions
 526        {
 527            Pattern = merged.Pattern,
 528            HttpVerbs = [HttpVerb.Get],
 529            ScriptCode = new LanguageOptions
 530            {
 531                Language = ScriptLanguage.Native,
 532            },
 533            AllowAnonymous = merged.AllowAnonymous,
 534            DisableAntiforgery = true,
 535            RequireSchemes = [.. merged.RequireSchemes],
 536            RequirePolicies = [.. merged.RequirePolicies],
 537            CorsPolicy = merged.CorsPolicy ?? string.Empty,
 538            RateLimitPolicyName = merged.RateLimitPolicyName,
 539            ShortCircuit = merged.ShortCircuit,
 540            ShortCircuitStatusCode = merged.ShortCircuitStatusCode,
 541            ThrowOnDuplicate = merged.ThrowOnDuplicate,
 542        };
 543        mapOptions.OpenAPI.Add(HttpVerb.Get, new OpenAPIPathMetadata(pattern: merged.Pattern, mapOptions: mapOptions)
 544        {
 545            Summary = merged.OpenApiSummary,
 546            Description = merged.OpenApiDescription,
 547            OperationId = merged.OpenApiOperationId,
 548            Tags = merged.OpenApiTags
 549        });
 50
 51        // Auto-register endpoint only when enabled
 552        if (!merged.AutoRegisterEndpoint)
 53        {
 154            Logger.Debug("Health endpoint AutoRegisterEndpoint=false; skipping automatic mapping for pattern {Pattern}",
 155            return this;
 56        }
 57
 58        // If the app pipeline is already built/configured, map immediately; otherwise defer until build
 459        if (IsConfigured)
 60        {
 061            MapHealthEndpointImmediate(merged, mapOptions);
 062            return this;
 63        }
 64
 465        return Use(app => MapHealthEndpointImmediate(merged, mapOptions));
 66    }
 67
 68    /// <summary>
 69    /// Registers a GET endpoint (default <c>/health</c>) using a pre-configured <see cref="HealthEndpointOptions"/> ins
 70    /// </summary>
 71    /// <param name="options">A fully configured options object.</param>
 72    /// <returns>The <see cref="KestrunHost"/> instance for fluent chaining.</returns>
 73    public KestrunHost AddHealthEndpoint(HealthEndpointOptions options)
 74    {
 275        ArgumentNullException.ThrowIfNull(options);
 76
 277        return AddHealthEndpoint(dest => CopyHealthEndpointOptions(options, dest));
 78    }
 79
 80    /// <summary>
 81    /// Extracts tags from the HTTP request query parameters.
 82    /// </summary>
 83    /// <param name="request">The HTTP request containing query parameters.</param>
 84    /// <returns>An array of extracted tags.</returns>
 85    private static string[] ExtractTags(HttpRequest request)
 86    {
 987        var collected = new List<string>();
 988        if (request.Query.TryGetValue("tag", out var singleValues))
 89        {
 3490            foreach (var value in singleValues)
 91            {
 1092                if (!string.IsNullOrEmpty(value))
 93                {
 994                    collected.AddRange(value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyE
 95                }
 96            }
 97        }
 98
 999        if (request.Query.TryGetValue("tags", out var multiValues))
 100        {
 16101            foreach (var value in multiValues)
 102            {
 4103                if (!string.IsNullOrEmpty(value))
 104                {
 4105                    collected.AddRange(value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyE
 106                }
 107            }
 108        }
 109
 9110        return collected.Count == 0
 9111            ? []
 18112            : [.. collected.Where(static t => !string.IsNullOrWhiteSpace(t))
 18113                           .Select(static t => t.Trim())
 9114                           .Distinct(StringComparer.OrdinalIgnoreCase)];
 115    }
 116
 117    /// <summary>
 118    /// Copies health endpoint options from source to target.
 119    /// </summary>
 120    /// <param name="source">The source HealthEndpointOptions instance.</param>
 121    /// <param name="target">The target HealthEndpointOptions instance.</param>
 122    private static void CopyHealthEndpointOptions(HealthEndpointOptions source, HealthEndpointOptions target)
 123    {
 7124        ArgumentNullException.ThrowIfNull(source);
 6125        ArgumentNullException.ThrowIfNull(target);
 126
 5127        target.Pattern = source.Pattern;
 5128        target.DefaultTags = source.DefaultTags is { Length: > 0 } tags
 5129            ? [.. tags]
 5130            : [];
 5131        target.AllowAnonymous = source.AllowAnonymous;
 5132        target.TreatDegradedAsUnhealthy = source.TreatDegradedAsUnhealthy;
 5133        target.ThrowOnDuplicate = source.ThrowOnDuplicate;
 5134        target.RequireSchemes = source.RequireSchemes is { Length: > 0 } schemes
 5135            ? [.. schemes]
 5136            : [];
 5137        target.RequirePolicies = source.RequirePolicies is { Length: > 0 } policies
 5138            ? [.. policies]
 5139            : [];
 5140        target.CorsPolicy = source.CorsPolicy;
 5141        target.RateLimitPolicyName = source.RateLimitPolicyName;
 5142        target.ShortCircuit = source.ShortCircuit;
 5143        target.ShortCircuitStatusCode = source.ShortCircuitStatusCode;
 5144        target.OpenApiSummary = source.OpenApiSummary;
 5145        target.OpenApiDescription = source.OpenApiDescription;
 5146        target.OpenApiOperationId = source.OpenApiOperationId;
 5147        target.OpenApiTags = source.OpenApiTags is { Count: > 0 } openApiTags
 5148            ? [.. openApiTags]
 5149            : [];
 5150        target.OpenApiGroupName = source.OpenApiGroupName;
 5151        target.MaxDegreeOfParallelism = source.MaxDegreeOfParallelism;
 5152        target.ProbeTimeout = source.ProbeTimeout;
 5153        target.AutoRegisterEndpoint = source.AutoRegisterEndpoint;
 5154        target.DefaultScriptLanguage = source.DefaultScriptLanguage;
 155        // BUGFIX: Ensure the response content type preference is propagated when using the overload
 156        // that accepts a pre-configured HealthEndpointOptions instance. Without this line the
 157        // ResponseContentType would always fall back to Json for PowerShell Add-KrHealthEndpoint
 158        // which calls AddHealthEndpoint(host, options) internally.
 5159        target.ResponseContentType = source.ResponseContentType;
 5160        target.XmlRootElementName = source.XmlRootElementName;
 5161        target.Compress = source.Compress;
 5162    }
 5163    private static int DetermineStatusCode(ProbeStatus status, bool treatDegradedAsUnhealthy) => status switch
 5164    {
 1165        ProbeStatus.Healthy => StatusCodes.Status200OK,
 3166        ProbeStatus.Degraded when !treatDegradedAsUnhealthy => StatusCodes.Status200OK,
 3167        _ => StatusCodes.Status503ServiceUnavailable
 5168    };
 169
 170    /// <summary>
 171    /// Maps the health endpoint immediately.
 172    /// </summary>
 173    /// <param name="merged">The merged HealthEndpointOptions instance.</param>
 174    /// <param name="mapOptions">The route mapping options.</param>
 175    /// <exception cref="InvalidOperationException">Thrown if a route with the same pattern and HTTP verb already exists
 176    private void MapHealthEndpointImmediate(HealthEndpointOptions merged, MapRouteOptions mapOptions)
 177    {
 0178        if (this.MapExists(mapOptions.Pattern!, HttpVerb.Get))
 179        {
 0180            var message = $"Route '{mapOptions.Pattern}' (GET) already exists. Skipping health endpoint registration.";
 0181            if (merged.ThrowOnDuplicate)
 182            {
 0183                throw new InvalidOperationException(message);
 184            }
 0185            Logger.Warning(message);
 0186            return;
 187        }
 188
 189        // Acquire WebApplication (throws if Build() truly has not executed yet). Using App here allows
 190        // early AddHealthEndpoint calls before EnableConfiguration via deferred middleware.
 0191        var endpoints = App;
 0192        var endpointLogger = Logger.ForContext("HealthEndpoint", merged.Pattern);
 193
 0194        var map = endpoints.MapMethods(merged.Pattern, [HttpMethods.Get], async context =>
 0195        {
 0196            var requestTags = ExtractTags(context.Request);
 0197            var tags = requestTags.Length > 0 ? requestTags : merged.DefaultTags;
 0198            var snapshot = GetHealthProbesSnapshot();
 0199
 0200            var report = await HealthProbeRunner.RunAsync(
 0201                probes: snapshot,
 0202                tagFilter: tags,
 0203                perProbeTimeout: merged.ProbeTimeout,
 0204                maxDegreeOfParallelism: merged.MaxDegreeOfParallelism,
 0205                logger: endpointLogger,
 0206                ct: context.RequestAborted).ConfigureAwait(false);
 0207
 0208            var krContext = new KestrunContext(this, context);
 0209            var response = krContext.Response;
 0210            response.CacheControl = new CacheControlHeaderValue
 0211            {
 0212                NoCache = true,
 0213                NoStore = true,
 0214                MustRevalidate = true,
 0215                MaxAge = TimeSpan.Zero
 0216            };
 0217
 0218            context.Response.Headers.Pragma = "no-cache";
 0219            context.Response.Headers.Expires = "0";
 0220
 0221            var statusCode = DetermineStatusCode(report.Status, merged.TreatDegradedAsUnhealthy);
 0222            switch (merged.ResponseContentType)
 0223            {
 0224                case HealthEndpointContentType.Json:
 0225                    await response.WriteJsonResponseAsync(report, depth: 10, compress: merged.Compress, statusCode: stat
 0226                    break;
 0227                case HealthEndpointContentType.Yaml:
 0228                    await response.WriteYamlResponseAsync(report, statusCode).ConfigureAwait(false);
 0229                    break;
 0230                case HealthEndpointContentType.Xml:
 0231                    await response.WriteXmlResponseAsync(
 0232                        report,
 0233                        statusCode,
 0234                        rootElementName: merged.XmlRootElementName ?? "Response",
 0235                        compress: merged.Compress).ConfigureAwait(false);
 0236                    break;
 0237                case HealthEndpointContentType.Text:
 0238                    var text = HealthReportTextFormatter.Format(report);
 0239                    await response.WriteTextResponseAsync(text, statusCode, contentType: $"text/plain; charset={response
 0240                    break;
 0241                case HealthEndpointContentType.Auto:
 0242                default:
 0243                    await response.WriteResponseAsync(report, statusCode).ConfigureAwait(false);
 0244                    break;
 0245            }
 0246
 0247            await response.ApplyTo(context.Response).ConfigureAwait(false);
 0248        }).WithMetadata(new ScriptLanguageAttribute(ScriptLanguage.Native));
 249
 0250        this.AddMapOptions(map, mapOptions);
 0251        _registeredRoutes[(mapOptions.Pattern!, HttpVerb.Get)] = mapOptions;
 0252        Logger.Information("Registered health endpoint at {Pattern}", mapOptions.Pattern);
 0253    }
 254}

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

#LineLine coverage
 1using Kestrun.Localization;
 2using Kestrun.Middleware;
 3
 4namespace Kestrun.Hosting;
 5
 6/// <summary>
 7/// Extension methods for adding localization to the Kestrun host.
 8/// </summary>
 9public partial class KestrunHost
 10{
 11    /// <summary>
 12    /// Adds localization middleware using the specified options.
 13    /// </summary>
 14    /// <param name="configure">Optional configuration for localization options.</param>
 15    /// <returns>The configured host.</returns>
 16    public KestrunHost AddLocalization(
 17        Action<KestrunLocalizationOptions>? configure = null)
 18    {
 019        var options = new KestrunLocalizationOptions();
 020        configure?.Invoke(options);
 21
 022        return Use(app => app.UseKestrunLocalization(options));
 23    }
 24
 25    /// <summary>
 26    /// Adds localization middleware using the specified options instance.
 27    /// </summary>
 28    /// <param name="options">The localization options.</param>
 29    /// <returns>The configured host.</returns>
 30    public KestrunHost AddLocalization(KestrunLocalizationOptions options)
 31    {
 032        ArgumentNullException.ThrowIfNull(options);
 33
 034        return Use(app => app.UseKestrunLocalization(options));
 35    }
 36}

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Hosting/KestrunHost_Razor.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// Kestrun uses Razor runtime compilation intentionally to support
 12// PowerShell-backed Razor pages (dynamic execution model).
 13// .NET 10 marks runtime compilation obsolete (ASPDEPR003); we suppress here by design.
 14// Track removal/alternative: https://aka.ms/aspnet/deprecate/003
 15
 16#if NET10_0_OR_GREATER
 17#pragma warning disable ASPDEPR003
 18#endif
 19
 20/// <summary>
 21/// Provides extension methods for adding PowerShell and Razor Pages to a Kestrun
 22/// </summary>
 23public partial class KestrunHost
 24{
 25    /// <summary>
 26    /// Adds PowerShell Razor Pages to the application.
 27    /// This middleware allows you to serve Razor Pages using PowerShell scripts.
 28    /// </summary>
 29    /// <param name="rootPath">The root directory for the Razor Pages.</param>
 30    /// <param name="routePrefix">The route prefix to use for the PowerShell Razor Pages.</param>
 31    /// <param name="cfg">Configuration options for the Razor Pages.</param>
 32    /// <returns>The current KestrunHost instance.</returns>
 33    public KestrunHost AddPowerShellRazorPages(string? rootPath, PathString? routePrefix, RazorPagesOptions? cfg)
 34    {
 535        if (Logger.IsEnabled(LogEventLevel.Debug))
 36        {
 037            Logger.Debug("Adding PowerShell Razor Pages with route prefix: {RoutePrefix}, config: {@Config}", routePrefi
 38        }
 39
 540        return AddPowerShellRazorPages(rootPath, routePrefix, dest =>
 541            {
 242                if (cfg != null)
 543                {
 544                    // simple value properties are fine
 045                    dest.RootDirectory = cfg.RootDirectory;
 546
 547                    // copy conventions one‑by‑one (collection is read‑only)
 048                    foreach (var c in cfg.Conventions)
 549                    {
 050                        dest.Conventions.Add(c);
 551                    }
 552                }
 753            });
 54    }
 55
 56    /// <summary>
 57    /// Adds PowerShell Razor Pages to the application.
 58    /// This middleware allows you to serve Razor Pages using PowerShell scripts.
 59    /// </summary>
 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(PathString? routePrefix) =>
 063        AddPowerShellRazorPages(rootPath: null, routePrefix: routePrefix, cfg: null as RazorPagesOptions);
 64
 65    /// <summary>
 66    /// Adds PowerShell Razor Pages to the application.
 67    /// </summary>
 68    /// <param name="rootPath">The root directory for the Razor Pages.</param>
 69    /// <param name="routePrefix">The route prefix to use for the PowerShell Razor Pages.</param>
 70    /// <returns>The current KestrunHost instance.</returns>
 71    public KestrunHost AddPowerShellRazorPages(string? rootPath, PathString? routePrefix) =>
 172            AddPowerShellRazorPages(rootPath: rootPath, routePrefix: routePrefix, cfg: null as RazorPagesOptions);
 73
 74    /// <summary>
 75    /// Adds PowerShell Razor Pages to the application with default configuration and no route prefix.
 76    /// </summary>
 77    /// <returns>The current KestrunHost instance.</returns>
 78    public KestrunHost AddPowerShellRazorPages() =>
 279        AddPowerShellRazorPages(rootPath: null, routePrefix: null, cfg: null as RazorPagesOptions);
 80
 81    /// <summary>
 82    /// Adds PowerShell Razor Pages to the application.
 83    /// </summary>
 84    /// <param name="rootPath">The root directory for the Razor Pages.</param>
 85    /// <returns>The current KestrunHost instance.</returns>
 86    public KestrunHost AddPowerShellRazorPages(string? rootPath) =>
 187        AddPowerShellRazorPages(rootPath: rootPath, routePrefix: null, cfg: null as RazorPagesOptions);
 88
 89    /// <summary>
 90    /// Adds PowerShell Razor Pages to the application.
 91    /// This middleware allows you to serve Razor Pages using PowerShell scripts.
 92    /// </summary>
 93    /// <param name="rootPath">The root directory for the Razor Pages.</param>
 94    /// <param name="routePrefix">The route prefix to use for the PowerShell Razor Pages.</param>
 95    /// <param name="cfg">Configuration options for the Razor Pages.</param>
 96    /// <returns>The current KestrunHost instance.</returns>
 97    public KestrunHost AddPowerShellRazorPages(string? rootPath, PathString? routePrefix, Action<RazorPagesOptions>? cfg
 98    {
 599        LogAddPowerShellRazorPages(routePrefix, cfg);
 100
 5101        var env = Builder.Environment;
 5102        var isDefaultPath = string.IsNullOrWhiteSpace(rootPath);
 5103        var pagesRootPath = ResolvePagesRootPath(env.ContentRootPath, rootPath, isDefaultPath);
 5104        var rootDirectory = ResolveRazorRootDirectory(env.ContentRootPath, pagesRootPath, isDefaultPath);
 105
 5106        _ = AddService(services =>
 5107        {
 2108            LogAddPowerShellRazorPagesService(routePrefix);
 5109
 2110            var mvcBuilder = ConfigureRazorPages(services, rootDirectory, cfg);
 2111            _ = mvcBuilder.AddRazorRuntimeCompilation();
 5112
 2113            ConfigureRuntimeCompilationReferences(services, pagesRootPath);
 7114        });
 115
 5116        return Use(app =>
 5117        {
 2118            ArgumentNullException.ThrowIfNull(RunspacePool);
 2119            LogAddPowerShellRazorPagesMiddleware(routePrefix);
 5120
 2121            MapPowerShellRazorPages(app, RunspacePool, pagesRootPath, routePrefix);
 5122
 2123            LogAddPowerShellRazorPagesMiddlewareAdded(routePrefix);
 7124        });
 125    }
 126
 127    /// <summary>
 128    /// Logs that PowerShell Razor Pages are being added.
 129    /// </summary>
 130    /// <param name="routePrefix">Optional route prefix for mounting Razor Pages.</param>
 131    /// <param name="cfg">Optional Razor Pages configuration delegate.</param>
 132    private void LogAddPowerShellRazorPages(PathString? routePrefix, Action<RazorPagesOptions>? cfg)
 133    {
 5134        if (Logger.IsEnabled(LogEventLevel.Debug))
 135        {
 0136            Logger.Debug("Adding PowerShell Razor Pages with route prefix: {RoutePrefix}, config: {@Config}", routePrefi
 137        }
 5138    }
 139
 140    /// <summary>
 141    /// Logs that PowerShell Razor Pages services are being added.
 142    /// </summary>
 143    /// <param name="routePrefix">Optional route prefix for mounting Razor Pages.</param>
 144    private void LogAddPowerShellRazorPagesService(PathString? routePrefix)
 145    {
 2146        if (Logger.IsEnabled(LogEventLevel.Debug))
 147        {
 0148            Logger.Debug("Adding PowerShell Razor Pages to the service with route prefix: {RoutePrefix}", routePrefix);
 149        }
 2150    }
 151
 152    /// <summary>
 153    /// Logs that PowerShell Razor Pages middleware is being added.
 154    /// </summary>
 155    /// <param name="routePrefix">Optional route prefix for mounting Razor Pages.</param>
 156    private void LogAddPowerShellRazorPagesMiddleware(PathString? routePrefix)
 157    {
 2158        if (Logger.IsEnabled(LogEventLevel.Debug))
 159        {
 0160            Logger.Debug("Adding PowerShell Razor Pages middleware with route prefix: {RoutePrefix}", routePrefix);
 161        }
 2162    }
 163
 164    /// <summary>
 165    /// Logs that PowerShell Razor Pages middleware has been added.
 166    /// </summary>
 167    /// <param name="routePrefix">Optional route prefix for mounting Razor Pages.</param>
 168    private void LogAddPowerShellRazorPagesMiddlewareAdded(PathString? routePrefix)
 169    {
 2170        if (Logger.IsEnabled(LogEventLevel.Debug))
 171        {
 0172            Logger.Debug("PowerShell Razor Pages middleware added with route prefix: {RoutePrefix}", routePrefix);
 173        }
 2174    }
 175
 176    /// <summary>
 177    /// Resolves the filesystem path used as the Pages root.
 178    /// </summary>
 179    /// <param name="contentRootPath">The application content root path.</param>
 180    /// <param name="rootPath">Optional explicit Pages root path.</param>
 181    /// <param name="isDefaultPath">Whether <paramref name="rootPath"/> was not provided.</param>
 182    /// <returns>The resolved Pages root path.</returns>
 183    private string ResolvePagesRootPath(string contentRootPath, string? rootPath, bool isDefaultPath)
 184    {
 5185        return isDefaultPath
 5186            ? Path.Combine(contentRootPath, "Pages")
 5187            : rootPath!;
 188    }
 189
 190    /// <summary>
 191    /// Resolves the Razor Pages <see cref="RazorPagesOptions.RootDirectory"/> value.
 192    /// </summary>
 193    /// <param name="contentRootPath">The application content root path.</param>
 194    /// <param name="pagesRootPath">The resolved Pages root filesystem path.</param>
 195    /// <param name="isDefaultPath">Whether the Pages root is the default path.</param>
 196    /// <returns>The RootDirectory value to apply, or <c>null</c> if no override should be applied.</returns>
 197    private string? ResolveRazorRootDirectory(string contentRootPath, string pagesRootPath, bool isDefaultPath)
 198    {
 5199        if (isDefaultPath)
 200        {
 3201            return null;
 202        }
 203
 2204        var relative = Path.GetRelativePath(contentRootPath, pagesRootPath)
 2205            .Replace("\\", "/");
 2206        return "/" + relative;
 207    }
 208
 209    /// <summary>
 210    /// Configures Razor Pages and applies optional RootDirectory and user configuration.
 211    /// </summary>
 212    /// <param name="services">The service collection.</param>
 213    /// <param name="rootDirectory">Optional Razor Pages root directory (virtual path) to apply.</param>
 214    /// <param name="cfg">Optional user configuration delegate for Razor Pages options.</param>
 215    /// <returns>The MVC builder instance.</returns>
 216    private static IMvcBuilder ConfigureRazorPages(IServiceCollection services, string? rootDirectory, Action<RazorPages
 217    {
 2218        var mvcBuilder = services.AddRazorPages();
 219
 2220        if (!string.IsNullOrWhiteSpace(rootDirectory))
 221        {
 2222            _ = mvcBuilder.AddRazorPagesOptions(opts => opts.RootDirectory = rootDirectory);
 223        }
 224
 2225        if (cfg != null)
 226        {
 2227            _ = mvcBuilder.AddRazorPagesOptions(cfg);
 228        }
 229
 2230        return mvcBuilder;
 231    }
 232    /// <summary>
 233    /// Configures runtime compilation reference paths and optional file watching for the Pages directory.
 234    /// </summary>
 235    /// <param name="services">The service collection.</param>
 236    /// <param name="pagesRootPath">The resolved Pages directory path.</param>
 237    private void ConfigureRuntimeCompilationReferences(IServiceCollection services, string pagesRootPath)
 238    {
 2239        _ = services.Configure<MvcRazorRuntimeCompilationOptions>(opts =>
 2240        {
 2241            AddLoadedAssemblyReferences(opts);
 2242            AddSharedFrameworkReferences(opts);
 2243            AddPagesFileProviderIfExists(opts, pagesRootPath);
 4244        });
 2245    }
 246
 247    /// <summary>
 248    /// Adds already-loaded managed assemblies as Roslyn reference paths.
 249    /// </summary>
 250    /// <param name="opts">Runtime compilation options to update.</param>
 251    private void AddLoadedAssemblyReferences(MvcRazorRuntimeCompilationOptions opts)
 252    {
 1094253        foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()
 727254            .Where(a => !a.IsDynamic && IsManaged(a.Location)))
 255        {
 545256            opts.AdditionalReferencePaths.Add(asm.Location);
 257        }
 2258    }
 259
 260    /// <summary>
 261    /// Adds managed DLLs from the shared framework directory as Roslyn reference paths.
 262    /// </summary>
 263    /// <param name="opts">Runtime compilation options to update.</param>
 264    private void AddSharedFrameworkReferences(MvcRazorRuntimeCompilationOptions opts)
 265    {
 2266        var coreDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!;
 676267        foreach (var dll in Directory.EnumerateFiles(coreDir, "*.dll").Where(IsManaged))
 268        {
 336269            opts.AdditionalReferencePaths.Add(dll);
 270        }
 2271    }
 272
 273    /// <summary>
 274    /// Adds a file provider for the Pages directory so Razor runtime compilation can watch changes.
 275    /// </summary>
 276    /// <param name="opts">Runtime compilation options to update.</param>
 277    /// <param name="pagesRootPath">The resolved Pages directory path.</param>
 278    private static void AddPagesFileProviderIfExists(MvcRazorRuntimeCompilationOptions opts, string pagesRootPath)
 279    {
 2280        if (Directory.Exists(pagesRootPath))
 281        {
 2282            opts.FileProviders.Add(new PhysicalFileProvider(pagesRootPath));
 283        }
 2284    }
 285
 286    /// <summary>
 287    /// Maps PowerShell Razor Pages middleware either at the application root or under a route prefix.
 288    /// </summary>
 289    /// <param name="app">The application builder.</param>
 290    /// <param name="pool">The runspace pool manager for PowerShell execution.</param>
 291    /// <param name="pagesRootPath">The resolved Pages directory path.</param>
 292    /// <param name="routePrefix">Optional route prefix for mounting Razor Pages.</param>
 293    private void MapPowerShellRazorPages(IApplicationBuilder app, KestrunRunspacePoolManager pool, string pagesRootPath,
 294    {
 2295        if (routePrefix.HasValue)
 296        {
 0297            _ = app.Map(routePrefix.Value, branch =>
 0298            {
 0299                _ = branch.UsePowerShellRazorPages(pool, pagesRootPath);
 0300                _ = branch.UseRouting();
 0301                _ = branch.UseEndpoints(e => e.MapRazorPages());
 0302            });
 303
 0304            return;
 305        }
 306
 2307        _ = app.UsePowerShellRazorPages(pool, pagesRootPath);
 2308        _ = app.UseRouting();
 4309        _ = app.UseEndpoints(e => e.MapRazorPages());
 2310    }
 311
 312    /// <summary>
 313    /// Adds Razor Pages to the application.
 314    /// </summary>
 315    /// <param name="cfg">The configuration options for Razor Pages.</param>
 316    /// <returns>The current KestrunHost instance.</returns>
 317    public KestrunHost AddRazorPages(RazorPagesOptions? cfg)
 318    {
 1319        if (Logger.IsEnabled(LogEventLevel.Debug))
 320        {
 0321            Logger.Debug("Adding Razor Pages from source: {Source}", cfg);
 322        }
 323
 1324        if (cfg == null)
 325        {
 0326            return AddRazorPages(); // no config, use defaults
 327        }
 328
 1329        return AddRazorPages(dest =>
 1330            {
 1331                // simple value properties are fine
 1332                dest.RootDirectory = cfg.RootDirectory;
 1333
 1334                // copy conventions one‑by‑one (collection is read‑only)
 2335                foreach (var c in cfg.Conventions)
 1336                {
 0337                    dest.Conventions.Add(c);
 1338                }
 2339            });
 340    }
 341
 342    /// <summary>
 343    /// Adds Razor Pages to the application.
 344    /// This overload allows you to specify configuration options.
 345    /// If you need to configure Razor Pages options, use the other overload.
 346    /// </summary>
 347    /// <param name="cfg">The configuration options for Razor Pages.</param>
 348    /// <returns>The current KestrunHost instance.</returns>
 349    public KestrunHost AddRazorPages(Action<RazorPagesOptions>? cfg = null)
 350    {
 3351        if (Logger.IsEnabled(LogEventLevel.Debug))
 352        {
 0353            Logger.Debug("Adding Razor Pages with configuration: {Config}", cfg);
 354        }
 355
 3356        return AddService(services =>
 3357        {
 3358            var mvc = services.AddRazorPages();         // returns IMvcBuilder
 3359
 3360            if (cfg != null)
 3361            {
 2362                _ = mvc.AddRazorPagesOptions(cfg);          // ← the correct extension
 3363            }
 3364            //  —OR—
 3365            // services.Configure(cfg);                 // also works
 3366        })
 6367         .Use(app => ((IEndpointRouteBuilder)app).MapRazorPages());// optional: automatically map Razor endpoints after 
 368    }
 369
 370    // helper: true  ⇢ file contains managed metadata
 371    private bool IsManaged(string path)
 372    {
 1934373        try { _ = AssemblyName.GetAssemblyName(path); return true; }
 344374        catch { return false; }          // native ⇒ BadImageFormatException
 1053375    }
 376}
 377
 378#if NET10_0_OR_GREATER
 379#pragma warning restore ASPDEPR003
 380#endif

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

#LineLine coverage
 1using Kestrun.Sse;
 2using Kestrun.Hosting.Options;
 3using Kestrun.Scripting;
 4using Kestrun.Utilities;
 5using Microsoft.Extensions.DependencyInjection.Extensions;
 6using Microsoft.OpenApi;
 7using System.Text.Json;
 8using System.Threading.Channels;
 9using Kestrun.OpenApi;
 10
 11namespace Kestrun.Hosting;
 12
 13/// <summary>
 14/// SSE broadcast extensions
 15/// </summary>
 16public partial class KestrunHost
 17{
 18    /// <summary>
 19    /// Adds an SSE broadcast endpoint that keeps a connection open and streams broadcast events.
 20    /// </summary>
 21    /// <param name="options">Optional OpenAPI customization for the broadcast endpoint route registry entry.</param>
 22    /// <returns>The host instance.</returns>
 23    public KestrunHost AddSseBroadcast(SseBroadcastOptions options)
 24    {
 25        // Allow callers to override metadata even if another component pre-registered the route.
 26        //    RegisterSseBroadcastRouteForOpenApi(options);
 27
 228        return AddService(services =>
 229            {
 230                services.TryAddSingleton(_ => Logger);
 231                services.TryAddSingleton<ISseBroadcaster, InMemorySseBroadcaster>();
 232            })
 233            .Use(app =>
 234            {
 235                RegisterSseBroadcastRouteForOpenApi(options);
 236                _ = ((IEndpointRouteBuilder)app).MapGet(
 237                    options.Path,
 238                    (HttpContext httpContext, IHostApplicationLifetime lifetime) =>
 239                        HandleSseBroadcastConnectAsync(httpContext, options.KeepAliveSeconds, lifetime));
 440            });
 41    }
 42
 43    /// <summary>
 44    /// Adds the SSE tag to the OpenAPI document if not already present.
 45    /// </summary>
 46    /// <param name="defTag">OpenAPI document descriptor to which the tag should be added.</param>
 47    private static void AddSseTag(OpenApiDocDescriptor defTag)
 48    {
 249        if (!defTag.ContainsTag(SseBroadcastOptions.DefaultTag))
 50        {
 251            _ = defTag.AddTag(name: SseBroadcastOptions.DefaultTag,
 252                 description: "Endpoints that stream server-to-client events using text/event-stream.",
 253                 summary: "Server-Sent Events",
 254                 parent: "Realtime",
 255                  externalDocs: new OpenApiExternalDocs
 256                  {
 257                      Description = "Server-Sent Events (MDN)",
 258                      Url = new Uri("https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events")
 259                  });
 60        }
 261    }
 62
 63    /// <summary>
 64    /// Registers the broadcast SSE endpoint in the host's route registry with OpenAPI metadata.
 65    /// This allows the OpenAPI generator to include the endpoint even though it is mapped directly
 66    /// through ASP.NET Core endpoint routing (not via <c>AddMapRoute</c>).
 67    /// </summary>
 68    /// <param name="options">Optional OpenAPI customization.</param>
 69    private void RegisterSseBroadcastRouteForOpenApi(SseBroadcastOptions options)
 70    {
 271        options ??= SseBroadcastOptions.Default;
 72        // Ensure the OpenAPI document descriptor exists (SSE broadcast can be configured even when OpenAPI is not).
 273        var apiDocDescriptors = GetOrCreateOpenApiDocument(options.DocId);
 274        var routeOptions = new MapRouteOptions
 275        {
 276            Pattern = options.Path,
 277            HttpVerbs = [HttpVerb.Get],
 278            ScriptCode = new LanguageOptions
 279            {
 280                Language = ScriptLanguage.Native,
 281                Code = string.Empty
 282            }
 283        };
 84
 285        if (!options.SkipOpenApi)
 86        {
 287            var mediaType = new OpenApiMediaType
 288            {
 289                ItemSchema = apiDocDescriptors[0].InferPrimitiveSchema(options.ItemSchemaType)
 290            };
 91
 292            var meta = new OpenAPIPathMetadata(pattern: options.Path, mapOptions: routeOptions)
 293            {
 294                OperationId = string.IsNullOrWhiteSpace(options.OperationId) ? null : options.OperationId,
 295                Summary = string.IsNullOrWhiteSpace(options.Summary) ? null : options.Summary,
 296                Description = string.IsNullOrWhiteSpace(options.Description) ? null : options.Description,
 297                Tags = [.. options.Tags],
 298                Responses = new OpenApiResponses
 299                {
 2100                    [options.StatusCode] = new OpenApiResponse
 2101                    {
 2102                        Description = string.IsNullOrWhiteSpace(options.ResponseDescription)
 2103                            ? "SSE stream (text/event-stream)"
 2104                            : options.ResponseDescription,
 2105                        Content = new Dictionary<string, IOpenApiMediaType>(StringComparer.Ordinal)
 2106                        {
 2107                            [options.ContentType] = mediaType
 2108                        }
 2109                    }
 2110                }
 2111            };
 2112            if (options.Tags.Contains(SseBroadcastOptions.DefaultTag))
 113            {
 8114                foreach (var defTag in apiDocDescriptors)
 115                {
 116                    // Ensure default tags are present
 2117                    AddRealTimeTag(defTag);
 2118                    AddSseTag(defTag);
 119                }
 120            }
 121
 2122            routeOptions.OpenAPI[HttpVerb.Get] = meta;
 123        }
 2124        _registeredRoutes[(options.Path, HttpVerb.Get)] = routeOptions;
 2125    }
 126
 127    /// <summary>
 128    /// Gets the number of currently connected SSE broadcast clients, if available.
 129    /// </summary>
 130    /// <returns>Connected client count, or null when the app is not built or SSE broadcaster is not registered.</return
 131    public int? GetSseConnectedClientCount()
 132    {
 133        try
 134        {
 0135            var svcProvider = App?.Services;
 0136            return svcProvider?.GetService(typeof(ISseBroadcaster)) is ISseBroadcaster b ? b.ConnectedCount : null;
 137        }
 0138        catch (InvalidOperationException)
 139        {
 0140            return null;
 141        }
 0142    }
 143
 144    /// <summary>
 145    /// Broadcasts an SSE event to all connected clients.
 146    /// </summary>
 147    /// <param name="eventName">Event name (optional).</param>
 148    /// <param name="data">Event payload.</param>
 149    /// <param name="id">Optional event ID.</param>
 150    /// <param name="retryMs">Optional reconnect interval in milliseconds.</param>
 151    /// <param name="cancellationToken">Cancellation token.</param>
 152    /// <returns>True when broadcast succeeded (or no clients); false when broadcaster isn't configured.</returns>
 153    public async Task<bool> BroadcastSseEventAsync(string? eventName, string data, string? id = null, int? retryMs = nul
 154    {
 155        try
 156        {
 0157            var svcProvider = App?.Services;
 0158            if (svcProvider == null)
 159            {
 0160                Logger.Warning("No service provider available to resolve ISseBroadcaster.");
 0161                return false;
 162            }
 163
 0164            if (svcProvider.GetService(typeof(ISseBroadcaster)) is not ISseBroadcaster broadcaster)
 165            {
 0166                Logger.Warning("ISseBroadcaster service is not registered. Make sure SSE broadcast is configured.");
 0167                return false;
 168            }
 169
 0170            await broadcaster.BroadcastAsync(eventName, data, id, retryMs, cancellationToken).ConfigureAwait(false);
 0171            return true;
 172        }
 0173        catch (InvalidOperationException)
 174        {
 0175            Logger.Warning("WebApplication is not built yet. Call Build() first.");
 0176            return false;
 177        }
 0178        catch (Exception ex)
 179        {
 0180            Logger.Error(ex, "Failed to broadcast SSE event: {EventName}", eventName);
 0181            return false;
 182        }
 0183    }
 184
 185    /// <summary>
 186    /// Handles a broadcast SSE connection (server keeps response open and streams broadcast events).
 187    /// </summary>
 188    /// <param name="httpContext">ASP.NET Core HTTP context.</param>
 189    /// <param name="keepAliveSeconds">Keep-alive interval in seconds.</param>
 190    /// <param name="lifetime">Host application lifetime.</param>
 191    private async Task HandleSseBroadcastConnectAsync(HttpContext httpContext, int keepAliveSeconds,
 192    IHostApplicationLifetime lifetime)
 193    {
 194        // Link request cancellation with application stopping to ensure graceful shutdown.
 0195        using var linked = CancellationTokenSource.CreateLinkedTokenSource(
 0196            httpContext.RequestAborted,
 0197            lifetime.ApplicationStopping);
 0198        var ct = linked.Token;
 199        // Set SSE response headers
 0200        if (httpContext.RequestServices.GetService(typeof(ISseBroadcaster)) is not ISseBroadcaster broadcaster)
 201        {
 0202            httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
 0203            await httpContext.Response.WriteAsync("SSE broadcaster is not configured.", ct).ConfigureAwait(false);
 0204            return;
 205        }
 206
 0207        var ctx = new Models.KestrunContext(this, httpContext);
 0208        ctx.StartSse();
 209
 0210        var subscription = broadcaster.Subscribe(httpContext.RequestAborted);
 211
 0212        var connectedJson = JsonSerializer.Serialize(new
 0213        {
 0214            clientId = subscription.ClientId,
 0215            serverTime = DateTimeOffset.UtcNow
 0216        });
 217
 0218        await ctx.WriteSseEventAsync("connected", connectedJson, id: null, retryMs: 2000, ct: ct).ConfigureAwait(false);
 0219        await PumpSseAsync(httpContext, subscription.Reader, keepAliveSeconds, ct).ConfigureAwait(false);
 0220    }
 221
 222    /// <summary>
 223    /// Pumps formatted SSE payloads from a channel to the HTTP response, with optional keep-alives.
 224    /// </summary>
 225    /// <param name="httpContext">HTTP context.</param>
 226    /// <param name="reader">Channel reader.</param>
 227    /// <param name="keepAliveSeconds">Keep-alive interval in seconds.</param>
 228    /// <param name="ct">Cancellation token.</param>
 229    private static async Task PumpSseAsync(HttpContext httpContext, ChannelReader<string> reader, int keepAliveSeconds, 
 230    {
 0231        var keepAliveTask = keepAliveSeconds > 0
 0232            ? Task.Delay(TimeSpan.FromSeconds(keepAliveSeconds), ct)
 0233            : null;
 234
 235        try
 236        {
 0237            while (!ct.IsCancellationRequested)
 238            {
 0239                var readTask = reader.ReadAsync(ct).AsTask();
 240
 0241                if (keepAliveTask == null)
 242                {
 0243                    var payload = await readTask.ConfigureAwait(false);
 0244                    await WritePayloadAsync(httpContext, payload, ct).ConfigureAwait(false);
 0245                    continue;
 246                }
 247
 0248                var completed = await Task.WhenAny(readTask, keepAliveTask).ConfigureAwait(false);
 249
 0250                if (completed == readTask)
 251                {
 0252                    var payload = await readTask.ConfigureAwait(false);
 0253                    await WritePayloadAsync(httpContext, payload, ct).ConfigureAwait(false);
 254                }
 255                else
 256                {
 0257                    await WritePayloadAsync(httpContext, SseEventFormatter.FormatComment($"keep-alive {DateTimeOffset.Ut
 0258                    keepAliveTask = Task.Delay(TimeSpan.FromSeconds(keepAliveSeconds), ct);
 259                }
 0260            }
 0261        }
 0262        catch (OperationCanceledException)
 263        {
 264            // Client disconnected.
 0265        }
 0266        catch (ChannelClosedException)
 267        {
 268            // Server removed the client.
 0269        }
 270        finally
 271        {
 272            // No-op; keepAliveTask is GC'd.
 273        }
 0274    }
 275
 276    /// <summary>
 277    /// Writes a pre-formatted SSE payload string to the response and flushes.
 278    /// </summary>
 279    /// <param name="httpContext">HTTP context.</param>
 280    /// <param name="payload">Pre-formatted payload.</param>
 281    /// <param name="cancellationToken">Cancellation token.</param>
 282    private static async Task WritePayloadAsync(HttpContext httpContext, string payload, CancellationToken cancellationT
 283    {
 0284        await httpContext.Response.WriteAsync(payload, cancellationToken).ConfigureAwait(false);
 0285        await httpContext.Response.Body.FlushAsync(cancellationToken).ConfigureAwait(false);
 0286    }
 287}

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

#LineLine coverage
 1
 2using Kestrun.Middleware;
 3using Microsoft.Net.Http.Headers;
 4using Serilog.Events;
 5
 6namespace Kestrun.Hosting;
 7
 8/// <summary>
 9/// Provides methods for configuring static file, default file, favicon, and file server middleware in Kestrun
 10/// </summary>
 11public partial class KestrunHost
 12{
 13    /// <summary>
 14    /// Adds default files middleware to the application.
 15    /// This middleware serves default files like index.html when a directory is requested.
 16    /// </summary>
 17    /// <param name="cfg">Configuration options for the default files middleware.</param>
 18    /// <returns>The current KestrunHost instance.</returns>
 19    public KestrunHost AddDefaultFiles(DefaultFilesOptions? cfg)
 20    {
 221        if (Logger.IsEnabled(LogEventLevel.Debug))
 22        {
 023            Logger.Debug("Adding Default Files with configuration: {@Config}", cfg);
 24        }
 25
 226        if (cfg == null)
 27        {
 128            return AddDefaultFiles(); // no config, use defaults
 29        }
 30
 31        // Convert DefaultFilesOptions to an Action<DefaultFilesOptions>
 132        return AddDefaultFiles(options =>
 133        {
 034            CopyDefaultFilesOptions(cfg, options);
 135        });
 36    }
 37
 38    /// <summary>
 39    /// Adds default files middleware to the application.
 40    /// This middleware serves default files like index.html when a directory is requested.
 41    /// </summary>
 42    /// <param name="cfg">Configuration options for the default files middleware.</param>
 43    /// <returns>The current KestrunHost instance.</returns>
 44    public KestrunHost AddDefaultFiles(Action<DefaultFilesOptions>? cfg = null)
 45    {
 546        return Use(app =>
 547        {
 148            var options = new DefaultFilesOptions();
 149            cfg?.Invoke(options);
 150            _ = app.UseDefaultFiles(options);
 651        });
 52    }
 53
 54    /// <summary>
 55    /// Adds a directory browser middleware to the application.
 56    /// This middleware enables directory browsing for a specified request path.
 57    /// </summary>
 58    /// <param name="requestPath">The request path to enable directory browsing for.</param>
 59    /// <returns>The current KestrunHost instance.</returns>
 60    public KestrunHost AddDirectoryBrowser(string requestPath)
 61    {
 062        return Use(app =>
 063        {
 064            _ = app.UseDirectoryBrowser(requestPath);
 065        });
 66    }
 67
 68    /// <summary>
 69    /// Adds a favicon middleware to the application.
 70    /// </summary>
 71    /// <param name="iconPath">The path to the favicon file. If null, uses the default favicon.</param>
 72    /// <returns>The current KestrunHost instance.</returns>
 73    public KestrunHost AddFavicon(string? iconPath = null)
 74    {
 475        return Use(app =>
 476        {
 277            _ = app.UseFavicon(iconPath);
 678        });
 79    }
 80
 81    /// <summary>
 82    /// Copies static file options from one object to another.
 83    /// </summary>
 84    /// <param name="src">The source static file options.</param>
 85    /// <param name="dest">The destination static file options.</param>
 86    /// <remarks>
 87    /// This method copies properties from the source static file options to the destination static file options.
 88    /// </remarks>
 89    private static void CopyStaticFileOptions(StaticFileOptions? src, StaticFileOptions dest)
 90    {
 91        // If no source, return a new empty options object
 092        if (src == null || dest == null)
 93        {
 094            return;
 95        }
 96        // Copy properties from source to destination
 097        dest.ContentTypeProvider = src.ContentTypeProvider;
 098        dest.OnPrepareResponse = src.OnPrepareResponse;
 099        dest.ServeUnknownFileTypes = src.ServeUnknownFileTypes;
 0100        dest.DefaultContentType = src.DefaultContentType;
 0101        dest.FileProvider = src.FileProvider;
 0102        dest.RequestPath = src.RequestPath;
 0103        dest.RedirectToAppendTrailingSlash = src.RedirectToAppendTrailingSlash;
 0104        dest.HttpsCompression = src.HttpsCompression;
 0105    }
 106
 107    /// <summary>
 108    /// Copies default files options from one object to another.
 109    /// This method is used to ensure that the default files options are correctly configured.
 110    /// </summary>
 111    /// <param name="src">The source default files options.</param>
 112    /// <param name="dest">The destination default files options.</param>
 113    /// <remarks>
 114    /// This method copies properties from the source default files options to the destination default files options.
 115    /// </remarks>
 116    private static void CopyDefaultFilesOptions(DefaultFilesOptions? src, DefaultFilesOptions dest)
 117    {
 118        // If no source, return a new empty options object
 0119        if (src == null || dest == null)
 120        {
 0121            return;
 122        }
 123        // Copy properties from source to destination
 0124        dest.DefaultFileNames.Clear();
 0125        foreach (var name in src.DefaultFileNames)
 126        {
 0127            dest.DefaultFileNames.Add(name);
 128        }
 129
 0130        dest.FileProvider = src.FileProvider;
 0131        dest.RequestPath = src.RequestPath;
 0132        dest.RedirectToAppendTrailingSlash = src.RedirectToAppendTrailingSlash;
 0133    }
 134
 135    /// <summary>
 136    /// Adds a file server middleware to the application.
 137    /// </summary>
 138    /// <param name="cfg">Configuration options for the file server middleware.</param>
 139    /// <param name="cacheControl">Optional cache control headers to apply to static file responses.</param>
 140    /// <returns>The current KestrunHost instance.</returns>
 141    /// <remarks>
 142    /// This middleware serves static files and default files from a specified file provider.
 143    /// If no configuration is provided, it uses default settings.
 144    /// </remarks>
 145    public KestrunHost AddFileServer(FileServerOptions? cfg, CacheControlHeaderValue? cacheControl = null)
 146    {
 2147        if (Logger.IsEnabled(LogEventLevel.Debug))
 148        {
 0149            Logger.Debug("Adding File Server with configuration: {@Config}", cfg);
 150        }
 151
 2152        if (cfg == null)
 153        {
 1154            return AddFileServer(); // no config, use defaults
 155        }
 156
 157        // Convert FileServerOptions to an Action<FileServerOptions>
 1158        return AddFileServer(options =>
 1159        {
 0160            options.EnableDefaultFiles = cfg.EnableDefaultFiles;
 0161            options.EnableDirectoryBrowsing = cfg.EnableDirectoryBrowsing;
 0162            options.FileProvider = cfg.FileProvider;
 0163            options.RequestPath = cfg.RequestPath;
 0164            options.RedirectToAppendTrailingSlash = cfg.RedirectToAppendTrailingSlash;
 0165            CopyDefaultFilesOptions(cfg.DefaultFilesOptions, options.DefaultFilesOptions);
 0166            if (cfg.DirectoryBrowserOptions != null)
 1167            {
 0168                options.DirectoryBrowserOptions.FileProvider = cfg.DirectoryBrowserOptions.FileProvider;
 0169                options.DirectoryBrowserOptions.RequestPath = cfg.DirectoryBrowserOptions.RequestPath;
 0170                options.DirectoryBrowserOptions.RedirectToAppendTrailingSlash = cfg.DirectoryBrowserOptions.RedirectToAp
 1171            }
 1172
 0173            CopyStaticFileOptions(cfg.StaticFileOptions, options.StaticFileOptions);
 0174            if (cacheControl != null)
 1175            {
 0176                options.StaticFileOptions.OnPrepareResponse = ctx =>
 0177                {
 0178                    // Apply the provided cache control if one was given
 0179                    if (Logger.IsEnabled(LogEventLevel.Debug))
 0180                    {
 0181                        Logger.Debug("Setting Cache-Control header to: {@CacheControl}", cacheControl);
 0182                    }
 0183                    ctx.Context.Response.Headers.CacheControl = cacheControl.ToString();
 0184                };
 1185            }
 0186            else if (DefaultCacheControl != null)
 1187            {
 0188                options.StaticFileOptions.OnPrepareResponse = ctx =>
 0189                {
 0190                    // Apply the host-wide default cache control if no specific one was provided
 0191                    if (Logger.IsEnabled(LogEventLevel.Debug))
 0192                    {
 0193                        Logger.Debug("Setting host-wide default cache Cache-Control: {@DefaultCacheControl}", DefaultCac
 0194                    }
 0195                    ctx.Context.Response.Headers.CacheControl = DefaultCacheControl.ToString();
 0196                };
 1197            }
 1198        });
 199    }
 200
 201    /// <summary>
 202    /// Adds a file server middleware to the application.
 203    /// This middleware serves static files and default files from a specified file provider.
 204    /// </summary>
 205    /// <param name="cfg">Configuration options for the file server middleware.</param>
 206    /// <returns>The current KestrunHost instance.</returns>
 207    public KestrunHost AddFileServer(Action<FileServerOptions>? cfg = null)
 208    {
 7209        if (Logger.IsEnabled(LogEventLevel.Debug))
 210        {
 0211            Logger.Debug("Adding File Server with Action<configuration>");
 212        }
 213
 7214        return Use(app =>
 7215        {
 2216            var options = new FileServerOptions();
 2217            cfg?.Invoke(options);
 2218            _ = app.UseFileServer(options);
 9219        });
 220    }
 221
 222    /// <summary>
 223    /// Adds static files to the application.
 224    /// This overload allows you to specify configuration options.
 225    /// </summary>
 226    /// <param name="cfg">The static file options to configure.</param>
 227    /// <returns>The current KestrunHost instance.</returns>
 228    public KestrunHost AddStaticFiles(Action<StaticFileOptions>? cfg = null)
 229    {
 8230        if (Logger.IsEnabled(LogEventLevel.Debug))
 231        {
 0232            Logger.Debug("Adding static files with configuration: {Config}", cfg);
 233        }
 234
 8235        return Use(app =>
 8236        {
 3237            if (cfg == null)
 8238            {
 0239                _ = app.UseStaticFiles();
 8240            }
 8241            else
 8242            {
 3243                var options = new StaticFileOptions();
 3244                cfg(options);
 8245
 3246                _ = app.UseStaticFiles(options);
 8247            }
 11248        });
 249    }
 250
 251    /// <summary>
 252    /// Adds static files to the application.
 253    /// This overload allows you to specify configuration options.
 254    /// </summary>
 255    /// <param name="options">The static file options to configure.</param>
 256    /// <param name="cacheControl">Optional cache control headers to apply to static file responses.</param>
 257    /// <returns>The current KestrunHost instance.</returns>
 258    public KestrunHost AddStaticFiles(StaticFileOptions options, CacheControlHeaderValue? cacheControl = null)
 259    {
 2260        if (Logger.IsEnabled(LogEventLevel.Debug))
 261        {
 0262            Logger.Debug("Adding static files with options: {@Options} and cache control: {@CacheControl}", options, cac
 263        }
 264
 2265        if (options == null)
 266        {
 1267            return AddStaticFiles(); // no options, use defaults
 268        }
 269
 270        // reuse the delegate overload so the pipeline logic stays in one place
 1271        return AddStaticFiles(o =>
 1272        {
 1273            // copy only the properties callers are likely to set
 0274            CopyStaticFileOptions(options, o);
 0275            if (cacheControl != null)
 1276            {
 0277                o.OnPrepareResponse = ctx =>
 0278                {
 0279                    // Apply the provided cache control if one was given
 0280                    if (Logger.IsEnabled(LogEventLevel.Debug))
 0281                    {
 0282                        Logger.Debug("Setting Cache-Control header to: {@CacheControl}", cacheControl);
 0283                    }
 0284                    ctx.Context.Response.Headers.CacheControl = cacheControl.ToString();
 0285                };
 1286            }
 0287            else if (DefaultCacheControl != null)
 1288            {
 0289                o.OnPrepareResponse = ctx =>
 0290                {
 0291                    // Apply the host-wide default cache control if no specific one was provided
 0292                    if (Logger.IsEnabled(LogEventLevel.Debug))
 0293                    {
 0294                        Logger.Debug("Setting host-wide default cache Cache-Control: {@DefaultCacheControl}", DefaultCac
 0295                    }
 0296                    ctx.Context.Response.Headers.CacheControl = DefaultCacheControl.ToString();
 0297                };
 1298            }
 1299        });
 300    }
 301}

/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.Localization;
 16using Kestrun.Hosting.Options;
 17using System.Runtime.InteropServices;
 18using Microsoft.PowerShell;
 19using System.Net.Sockets;
 20using Microsoft.Net.Http.Headers;
 21using Kestrun.Authentication;
 22using Kestrun.Health;
 23using Kestrun.Tasks;
 24using Kestrun.Runtime;
 25using Kestrun.OpenApi;
 26using Microsoft.AspNetCore.Antiforgery;
 27using Kestrun.Callback;
 28using System.Text.Json;
 29using System.Text.Json.Serialization;
 30using Microsoft.OpenApi;
 31using System.Text.Json.Nodes;
 32
 33namespace Kestrun.Hosting;
 34
 35/// <summary>
 36/// Provides hosting and configuration for the Kestrun application, including service registration, middleware setup, an
 37/// </summary>
 38public partial class KestrunHost : IDisposable
 39{
 40    private const string KestrunVariableMarkerKey = "__kestrunVariable";
 41
 42    #region Static Members
 43    private static readonly JsonSerializerOptions JsonOptions;
 44
 45    static KestrunHost()
 46    {
 147        JsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
 148        {
 149            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
 150            WriteIndented = false
 151        };
 152        JsonOptions.Converters.Add(new JsonStringEnumConverter());
 153    }
 54    #endregion
 55
 56    #region Fields
 288257    internal WebApplicationBuilder Builder { get; }
 58
 59    private WebApplication? _app;
 60
 13161    internal WebApplication App => _app ?? throw new InvalidOperationException("WebApplication is not built yet. Call Bu
 62
 63    /// <summary>
 64    /// Gets the application name for the Kestrun host.
 65    /// </summary>
 266    public string ApplicationName => Options.ApplicationName ?? "KestrunApp";
 67
 68    /// <summary>
 69    /// Gets the configuration options for the Kestrun host.
 70    /// </summary>
 178171    public KestrunOptions Options { get; private set; } = new();
 72
 73    /// <summary>
 74    /// List of PowerShell module paths to be loaded.
 75    /// </summary>
 62076    private readonly List<string> _modulePaths = [];
 77
 78    /// <summary>
 79    /// Indicates whether the Kestrun host is stopping.
 80    /// </summary>
 81    private int _stopping; // 0 = running, 1 = stopping
 82
 83    /// <summary>
 84    /// Indicates whether the Kestrun host configuration has been applied.
 85    /// </summary>
 47986    public bool IsConfigured { get; private set; }
 87
 88    /// <summary>
 89    /// Gets the timestamp when the Kestrun host was started.
 90    /// </summary>
 1791    public DateTime? StartTime { get; private set; }
 92
 93    /// <summary>
 94    /// Gets the timestamp when the Kestrun host was stopped.
 95    /// </summary>
 1896    public DateTime? StopTime { get; private set; }
 97
 98    /// <summary>
 99    /// Gets the uptime duration of the Kestrun host.
 100    /// While running (no StopTime yet), this returns DateTime.UtcNow - StartTime.
 101    /// After stopping, it returns StopTime - StartTime.
 102    /// If StartTime is not set, returns null.
 103    /// </summary>
 104    public TimeSpan? Uptime =>
 0105        !StartTime.HasValue
 0106            ? null
 0107            : StopTime.HasValue
 0108                ? StopTime - StartTime
 0109                : DateTime.UtcNow - StartTime.Value;
 110    /// <summary>
 111    /// The runspace pool manager for PowerShell execution.
 112    /// </summary>
 113    private KestrunRunspacePoolManager? _runspacePool;
 114
 115    /// <summary>
 116    /// Status code options for configuring status code pages.
 117    /// </summary>
 118    private StatusCodeOptions? _statusCodeOptions;
 119    /// <summary>
 120    /// Exception options for configuring exception handling.
 121    /// </summary>
 122    private ExceptionOptions? _exceptionOptions;
 123    /// <summary>
 124    /// Forwarded headers options for configuring forwarded headers handling.
 125    /// </summary>
 126    private ForwardedHeadersOptions? _forwardedHeaderOptions;
 127
 8128    internal KestrunRunspacePoolManager RunspacePool => _runspacePool ?? throw new InvalidOperationException("Runspace p
 129
 130    // ── ✦ QUEUE #1 : SERVICE REGISTRATION ✦ ─────────────────────────────
 620131    private readonly List<Action<IServiceCollection>> _serviceQueue = [];
 132
 133    // ── ✦ QUEUE #2 : MIDDLEWARE STAGES ✦ ────────────────────────────────
 620134    private readonly List<Action<IApplicationBuilder>> _middlewareQueue = [];
 135
 727136    internal List<Action<KestrunHost>> FeatureQueue { get; } = [];
 137
 788138    internal List<IProbe> HealthProbes { get; } = [];
 139#if NET9_0_OR_GREATER
 140    private readonly Lock _healthProbeLock = new();
 141#else
 620142    private readonly object _healthProbeLock = new();
 143#endif
 144
 620145    internal readonly Dictionary<(string Pattern, HttpVerb Method), MapRouteOptions> _registeredRoutes =
 620146    new(new RouteKeyComparer());
 147
 148    //internal readonly Dictionary<(string Scheme, string Type), AuthenticationSchemeOptions> _registeredAuthentications
 149    //  new(new AuthKeyComparer());
 150
 151    /// <summary>
 152    /// Gets the root directory path for the Kestrun application.
 153    /// </summary>
 145154    public string? KestrunRoot { get; private set; }
 155
 156    /// <summary>
 157    /// Gets the collection of module paths to be loaded by the Kestrun host.
 158    /// </summary>
 0159    public List<string> ModulePaths => _modulePaths;
 160
 161    /// <summary>
 162    /// Gets the shared state store for managing shared data across requests and sessions.
 163    /// </summary>
 202164    public SharedState.SharedState SharedState { get; }
 165
 166    /// <summary>
 167    /// Gets the Serilog logger instance used by the Kestrun host.
 168    /// </summary>
 11301169    public Serilog.ILogger Logger { get; private set; }
 170
 171    private SchedulerService? _scheduler;
 172    /// <summary>
 173    /// Gets the scheduler service used for managing scheduled tasks in the Kestrun host.
 174    /// Initialized in ConfigureServices via AddScheduler()
 175    /// </summary>
 176    public SchedulerService Scheduler
 177    {
 1178        get => _scheduler ?? throw new InvalidOperationException("SchedulerService is not initialized. Call AddScheduler
 1179        internal set => _scheduler = value;
 180    }
 181
 182    private KestrunTaskService? _tasks;
 183    /// <summary>
 184    /// Gets the ad-hoc task service used for running one-off tasks (PowerShell, C#, VB.NET).
 185    /// Initialized via AddTasks()
 186    /// </summary>
 187    public KestrunTaskService Tasks
 188    {
 0189        get => _tasks ?? throw new InvalidOperationException("Tasks is not initialized. Call AddTasks() to enable task m
 0190        internal set => _tasks = value;
 191    }
 192
 193    /// <summary>
 194    /// Gets the stack used for managing route groups in the Kestrun host.
 195    /// </summary>
 620196    public System.Collections.Stack RouteGroupStack { get; } = new();
 197
 198    /// <summary>
 199    /// Gets the registered routes in the Kestrun host.
 200    /// </summary>
 150201    public Dictionary<(string, HttpVerb), MapRouteOptions> RegisteredRoutes => _registeredRoutes;
 202
 203    /// <summary>
 204    /// Gets the registered authentication schemes in the Kestrun host.
 205    /// </summary>
 643206    public AuthenticationRegistry RegisteredAuthentications { get; } = new();
 207
 208    /// <summary>
 209    /// Gets or sets the default cache control settings for HTTP responses.
 210    /// </summary>
 9211    public CacheControlHeaderValue? DefaultCacheControl { get; internal set; }
 212
 213    /// <summary>
 214    /// Gets the shared state manager for managing shared data across requests and sessions.
 215    /// </summary>
 124216    public bool PowershellMiddlewareEnabled { get; set; } = false;
 217
 218    /// <summary>
 219    /// The localization store used by this host when `UseKestrunLocalization` is configured.
 220    /// May be null if localization middleware was not added.
 221    /// </summary>
 0222    public KestrunLocalizationStore? LocalizationStore { get; internal set; }
 223
 224    /// <summary>
 225    /// Gets or sets a value indicating whether this instance is the default Kestrun host.
 226    /// </summary>
 1227    public bool DefaultHost { get; internal set; }
 228
 229    /// <summary>
 230    /// The list of CORS policy names that have been defined in the KestrunHost instance.
 231    /// </summary>
 798232    public List<string> DefinedCorsPolicyNames { get; } = [];
 233
 234    /// <summary>
 235    /// Gets or sets a value indicating whether CORS (Cross-Origin Resource Sharing) is enabled.
 236    /// </summary>
 150237    public bool CorsPolicyDefined => DefinedCorsPolicyNames.Count > 0;
 238
 239    /// <summary>
 240    /// Gets the scanned OpenAPI component annotations from PowerShell scripts.
 241    /// </summary>
 59242    public Dictionary<string, OpenApiComponentAnnotationScanner.AnnotatedVariable>? ComponentAnnotations { get; private 
 243
 244    /// <summary>
 245    /// Gets or sets the status code options for configuring status code pages.
 246    /// </summary>
 247    public StatusCodeOptions? StatusCodeOptions
 248    {
 104249        get => _statusCodeOptions;
 250        set
 251        {
 0252            if (IsConfigured)
 253            {
 0254                throw new InvalidOperationException("Cannot modify StatusCodeOptions after configuration is applied.");
 255            }
 0256            _statusCodeOptions = value;
 0257        }
 258    }
 259
 260    /// <summary>
 261    /// Gets or sets the exception options for configuring exception handling.
 262    /// </summary>
 263    public ExceptionOptions? ExceptionOptions
 264    {
 115265        get => _exceptionOptions;
 266        set
 267        {
 5268            if (IsConfigured)
 269            {
 0270                throw new InvalidOperationException("Cannot modify ExceptionOptions after configuration is applied.");
 271            }
 5272            _exceptionOptions = value;
 5273        }
 274    }
 275
 276    /// <summary>
 277    /// Gets or sets the forwarded headers options for configuring forwarded headers handling.
 278    /// </summary>
 279    public ForwardedHeadersOptions? ForwardedHeaderOptions
 280    {
 107281        get => _forwardedHeaderOptions;
 282        set
 283        {
 4284            if (IsConfigured)
 285            {
 1286                throw new InvalidOperationException("Cannot modify ForwardedHeaderOptions after configuration is applied
 287            }
 3288            _forwardedHeaderOptions = value;
 3289        }
 290    }
 291
 292    /// <summary>
 293    /// Gets the antiforgery options for configuring antiforgery token generation and validation.
 294    /// </summary>
 0295    public AntiforgeryOptions? AntiforgeryOptions { get; set; }
 296
 297    /// <summary>
 298    /// Gets the OpenAPI document descriptor for configuring OpenAPI generation.
 299    /// </summary>
 727300    public Dictionary<string, OpenApiDocDescriptor> OpenApiDocumentDescriptor { get; } = [];
 301
 302    /// <summary>
 303    /// Gets the IDs of all OpenAPI documents configured in the Kestrun host.
 304    /// </summary>
 0305    public string[] OpenApiDocumentIds => [.. OpenApiDocumentDescriptor.Keys];
 306
 307    /// <summary>
 308    /// Gets the default OpenAPI document descriptor.
 309    /// </summary>
 310    public OpenApiDocDescriptor? DefaultOpenApiDocumentDescriptor
 0311        => OpenApiDocumentDescriptor.FirstOrDefault().Value;
 312
 313    #endregion
 314
 315    // Accepts optional module paths (from PowerShell)
 316    #region Constructor
 317
 318    /// <summary>
 319    /// Initializes a new instance of the <see cref="KestrunHost"/> class with the specified application name, root dire
 320    /// </summary>
 321    /// <param name="appName">The name of the application.</param>
 322    /// <param name="kestrunRoot">The root directory for the Kestrun application.</param>
 323    /// <param name="modulePathsObj">An array of module paths to be loaded.</param>
 324    public KestrunHost(string? appName, string? kestrunRoot = null, string[]? modulePathsObj = null) :
 108325            this(appName, Log.Logger, kestrunRoot, modulePathsObj)
 108326    { }
 327
 328    /// <summary>
 329    /// Initializes a new instance of the <see cref="KestrunHost"/> class with the specified application name and logger
 330    /// </summary>
 331    /// <param name="appName">The name of the application.</param>
 332    /// <param name="logger">The Serilog logger instance to use.</param>
 333    /// <param name="ordinalIgnoreCase">Indicates whether the shared state should be case-insensitive.</param>
 334    public KestrunHost(string? appName, Serilog.ILogger logger,
 0335          bool ordinalIgnoreCase) : this(appName, logger, null, null, null, ordinalIgnoreCase)
 0336    { }
 337
 338    /// <summary>
 339    /// Initializes a new instance of the <see cref="KestrunHost"/> class with the specified application name, logger, r
 340    /// </summary>
 341    /// <param name="appName">The name of the application.</param>
 342    /// <param name="logger">The Serilog logger instance to use.</param>
 343    /// <param name="kestrunRoot">The root directory for the Kestrun application.</param>
 344    /// <param name="modulePathsObj">An array of module paths to be loaded.</param>
 345    /// <param name="args">Command line arguments to pass to the application.</param>
 346    /// <param name="ordinalIgnoreCase">Indicates whether the shared state should be case-insensitive.</param>
 620347    public KestrunHost(string? appName, Serilog.ILogger logger,
 620348    string? kestrunRoot = null, string[]? modulePathsObj = null, string[]? args = null, bool ordinalIgnoreCase = true)
 349    {
 350        // ① Logger
 620351        Logger = logger ?? Log.Logger;
 620352        LogConstructorArgs(appName, logger == null, kestrunRoot, modulePathsObj?.Length ?? 0);
 620353        SharedState = new(ordinalIgnoreCase: ordinalIgnoreCase);
 354        // ② Working directory/root
 620355        SetWorkingDirectoryIfNeeded(kestrunRoot);
 356
 357        // ③ Ensure Kestrun module path is available
 620358        AddKestrunModulePathIfMissing(modulePathsObj);
 359
 360        // ④ WebApplicationBuilder
 361        // NOTE:
 362        // ASP.NET Core's WebApplicationBuilder validates that ContentRootPath exists.
 363        // On Unix/macOS, the process current working directory (CWD) can be deleted by tests or external code.
 364        // If we derive ContentRootPath from a missing/deleted directory, CreateBuilder throws.
 365        // We therefore (a) choose an existing directory when possible and (b) retry with a stable fallback
 366        // to keep host creation resilient in CI where test ordering/parallelism can surface this.
 367        WebApplicationOptions CreateWebAppOptions(string contentRootPath)
 368        {
 620369            return new()
 620370            {
 620371                ContentRootPath = contentRootPath,
 620372                Args = args ?? [],
 620373                EnvironmentName = EnvironmentHelper.Name
 620374            };
 375        }
 376
 620377        var contentRootPath = GetSafeContentRootPath(kestrunRoot);
 378
 379        try
 380        {
 620381            Builder = WebApplication.CreateBuilder(CreateWebAppOptions(contentRootPath));
 620382        }
 0383        catch (ArgumentException ex) when (
 0384            string.Equals(ex.ParamName, "contentRootPath", StringComparison.OrdinalIgnoreCase) &&
 0385            !string.Equals(contentRootPath, AppContext.BaseDirectory, StringComparison.Ordinal))
 386        {
 387            // The selected content root may have been deleted between resolution and builder initialization
 388            // (TOCTOU race) or the process CWD may have become invalid. Fall back to a stable path so host
 389            // creation does not fail.
 0390            Builder = WebApplication.CreateBuilder(CreateWebAppOptions(AppContext.BaseDirectory));
 0391        }
 392        // ✅ add here, after Builder is definitely assigned
 620393        _ = Builder.Services.Configure<HostOptions>(o =>
 620394        {
 104395            _ = o.ShutdownTimeout = TimeSpan.FromSeconds(5);
 724396        });
 397
 398        // Enable Serilog for the host
 620399        _ = Builder.Host.UseSerilog();
 400
 401        // Make this KestrunHost available via DI so framework-created components (e.g., auth handlers)
 402        // can resolve it. We register the current instance as a singleton.
 620403        _ = Builder.Services.AddSingleton(this);
 404
 405        // Expose Serilog.ILogger via DI for components (e.g., SignalR hubs) that depend on Serilog's logger
 406        // ASP.NET Core registers Microsoft.Extensions.Logging.ILogger by default; we also bind Serilog.ILogger
 407        // to the same instance so constructors like `KestrunHub(Serilog.ILogger logger)` resolve properly.
 620408        _ = Builder.Services.AddSingleton(Logger);
 409
 410        // ⑤ Options
 620411        InitializeOptions(appName);
 412
 413        // ⑥ Add user-provided module paths
 620414        AddUserModulePaths(modulePathsObj);
 415
 620416        Logger.Information("Current working directory: {CurrentDirectory}", GetSafeCurrentDirectory());
 620417    }
 418    #endregion
 419
 420    #region Helpers
 421
 422    /// <summary>
 423    /// Gets the OpenAPI document descriptor for the specified document ID.
 424    /// </summary>
 425    /// <param name="apiDocId">The ID of the OpenAPI document.</param>
 426    /// <returns>The OpenAPI document descriptor.</returns>
 427    public OpenApiDocDescriptor GetOrCreateOpenApiDocument(string apiDocId)
 428    {
 28429        if (string.IsNullOrWhiteSpace(apiDocId))
 430        {
 0431            throw new ArgumentException("Document ID cannot be null or whitespace.", nameof(apiDocId));
 432        }
 433        // Check if descriptor already exists
 28434        if (OpenApiDocumentDescriptor.TryGetValue(apiDocId, out var descriptor))
 435        {
 5436            if (Logger.IsEnabled(LogEventLevel.Debug))
 437            {
 5438                Logger.Debug("OpenAPI document descriptor for ID '{DocId}' already exists. Returning existing descriptor
 439            }
 440        }
 441        else
 442        {
 23443            descriptor = new OpenApiDocDescriptor(this, apiDocId);
 23444            OpenApiDocumentDescriptor[apiDocId] = descriptor;
 445        }
 28446        return descriptor;
 447    }
 448
 449    /// <summary>
 450    /// Gets the list of OpenAPI document descriptors for the specified document IDs.
 451    /// </summary>
 452    /// <param name="openApiDocIds"> The array of OpenAPI document IDs.</param>
 453    /// <returns>A list of OpenApiDocDescriptor objects corresponding to the provided document IDs.</returns>
 454    public List<OpenApiDocDescriptor> GetOrCreateOpenApiDocument(string[] openApiDocIds)
 455    {
 2456        var list = new List<OpenApiDocDescriptor>();
 8457        foreach (var apiDocId in openApiDocIds)
 458        {
 2459            list.Add(GetOrCreateOpenApiDocument(apiDocId));
 460        }
 2461        return list;
 462    }
 463
 464    /// <summary>
 465    /// Logs constructor arguments at Debug level for diagnostics.
 466    /// </summary>
 467    private void LogConstructorArgs(string? appName, bool defaultLogger, string? kestrunRoot, int modulePathsLength)
 468    {
 620469        if (Logger.IsEnabled(LogEventLevel.Debug))
 470        {
 454471            Logger.Debug(
 454472                "KestrunHost ctor: AppName={AppName}, DefaultLogger={DefaultLogger}, KestrunRoot={KestrunRoot}, ModulePa
 454473                appName, defaultLogger, kestrunRoot, modulePathsLength);
 474        }
 620475    }
 476
 477    /// <summary>
 478    /// Sets the current working directory to the provided Kestrun root if needed and stores it.
 479    /// </summary>
 480    /// <param name="kestrunRoot">The Kestrun root directory path.</param>
 481    private void SetWorkingDirectoryIfNeeded(string? kestrunRoot)
 482    {
 620483        if (string.IsNullOrWhiteSpace(kestrunRoot))
 484        {
 476485            return;
 486        }
 487
 144488        if (!string.Equals(GetSafeCurrentDirectory(), kestrunRoot, StringComparison.Ordinal))
 489        {
 107490            Directory.SetCurrentDirectory(kestrunRoot);
 107491            Logger.Information("Changed current directory to Kestrun root: {KestrunRoot}", kestrunRoot);
 492        }
 493        else
 494        {
 37495            Logger.Verbose("Current directory is already set to Kestrun root: {KestrunRoot}", kestrunRoot);
 496        }
 497
 144498        KestrunRoot = kestrunRoot;
 144499    }
 500
 501    private static string GetSafeContentRootPath(string? kestrunRoot)
 502    {
 620503        var candidate = !string.IsNullOrWhiteSpace(kestrunRoot)
 620504            ? kestrunRoot
 620505            : GetSafeCurrentDirectory();
 506
 507        // WebApplication.CreateBuilder requires that ContentRootPath exists.
 508        // On Unix/macOS, getcwd() can fail (or return a path that was deleted) if the CWD was removed.
 509        // This can happen in tests that use temp directories and delete them after constructing a host.
 510        // Guard here to avoid injecting a non-existent content root into ASP.NET Core.
 620511        return Directory.Exists(candidate)
 620512            ? candidate
 620513            : AppContext.BaseDirectory;
 514    }
 515
 516    private static string GetSafeCurrentDirectory()
 517    {
 518        try
 519        {
 1344520            return Directory.GetCurrentDirectory();
 521        }
 2522        catch (Exception ex) when (
 2523            ex is IOException or
 2524            UnauthorizedAccessException or
 2525            DirectoryNotFoundException or
 2526            FileNotFoundException)
 527        {
 528            // On Unix/macOS, getcwd() can fail with ENOENT if the CWD was deleted.
 529            // Fall back to the app base directory to keep host creation resilient.
 2530            return AppContext.BaseDirectory;
 531        }
 1344532    }
 533
 534    /// <summary>
 535    /// Ensures the core Kestrun module path is present; if missing, locates and adds it.
 536    /// </summary>
 537    /// <param name="modulePathsObj">The array of module paths to check.</param>
 538    private void AddKestrunModulePathIfMissing(string[]? modulePathsObj)
 539    {
 620540        var needsLocate = modulePathsObj is null ||
 657541                          (modulePathsObj?.Any(p => p.Contains("Kestrun.psm1", StringComparison.Ordinal)) == false);
 620542        if (!needsLocate)
 543        {
 37544            return;
 545        }
 546
 583547        var kestrunModulePath = PowerShellModuleLocator.LocateKestrunModule();
 583548        if (string.IsNullOrWhiteSpace(kestrunModulePath))
 549        {
 0550            Logger.Fatal("Kestrun module not found. Ensure the Kestrun module is installed.");
 0551            throw new FileNotFoundException("Kestrun module not found.");
 552        }
 553
 583554        Logger.Information("Found Kestrun module at: {KestrunModulePath}", kestrunModulePath);
 583555        Logger.Verbose("Adding Kestrun module path: {KestrunModulePath}", kestrunModulePath);
 583556        _modulePaths.Add(kestrunModulePath);
 583557    }
 558
 559    /// <summary>
 560    /// Initializes Kestrun options and sets the application name when provided.
 561    /// </summary>
 562    /// <param name="appName">The name of the application.</param>
 563    private void InitializeOptions(string? appName)
 564    {
 620565        if (string.IsNullOrEmpty(appName))
 566        {
 1567            Logger.Information("No application name provided, using default.");
 1568            Options = new KestrunOptions();
 569        }
 570        else
 571        {
 619572            Logger.Information("Setting application name: {AppName}", appName);
 619573            Options = new KestrunOptions { ApplicationName = appName };
 574        }
 619575    }
 576
 577    /// <summary>
 578    /// Adds user-provided module paths if they exist, logging warnings for invalid entries.
 579    /// </summary>
 580    /// <param name="modulePathsObj">The array of module paths to check.</param>
 581    private void AddUserModulePaths(string[]? modulePathsObj)
 582    {
 620583        if (modulePathsObj is IEnumerable<object> modulePathsEnum)
 584        {
 148585            foreach (var modPathObj in modulePathsEnum)
 586            {
 37587                if (modPathObj is string modPath && !string.IsNullOrWhiteSpace(modPath))
 588                {
 37589                    if (File.Exists(modPath))
 590                    {
 37591                        Logger.Information("[KestrunHost] Adding module path: {ModPath}", modPath);
 37592                        _modulePaths.Add(modPath);
 593                    }
 594                    else
 595                    {
 0596                        Logger.Warning("[KestrunHost] Module path does not exist: {ModPath}", modPath);
 597                    }
 598                }
 599                else
 600                {
 0601                    Logger.Warning("[KestrunHost] Invalid module path provided.");
 602                }
 603            }
 604        }
 620605    }
 606    #endregion
 607
 608    #region Health Probes
 609
 610    /// <summary>
 611    /// Registers the provided <see cref="IProbe"/> instance with the host.
 612    /// </summary>
 613    /// <param name="probe">The probe to register.</param>
 614    /// <returns>The current <see cref="KestrunHost"/> instance.</returns>
 615    public KestrunHost AddProbe(IProbe probe)
 616    {
 0617        ArgumentNullException.ThrowIfNull(probe);
 0618        RegisterProbeInternal(probe);
 0619        return this;
 620    }
 621
 622    /// <summary>
 623    /// Registers a delegate-based probe.
 624    /// </summary>
 625    /// <param name="name">Probe name.</param>
 626    /// <param name="tags">Optional tag list used for filtering.</param>
 627    /// <param name="callback">Delegate executed when the probe runs.</param>
 628    /// <returns>The current <see cref="KestrunHost"/> instance.</returns>
 629    public KestrunHost AddProbe(string name, string[]? tags, Func<CancellationToken, Task<ProbeResult>> callback)
 630    {
 0631        ArgumentException.ThrowIfNullOrEmpty(name);
 0632        ArgumentNullException.ThrowIfNull(callback);
 633
 0634        var probe = new DelegateProbe(name, tags, callback);
 0635        RegisterProbeInternal(probe);
 0636        return this;
 637    }
 638
 639    /// <summary>
 640    /// Registers a script-based probe written in any supported language.
 641    /// </summary>
 642    /// <param name="name">Probe name.</param>
 643    /// <param name="tags">Optional tag list used for filtering.</param>
 644    /// <param name="code">Script contents.</param>
 645    /// <param name="language">Optional language override. When null, <see cref="KestrunOptions.Health"/> defaults are u
 646    /// <param name="arguments">Optional argument dictionary exposed to the script.</param>
 647    /// <param name="extraImports">Optional language-specific imports.</param>
 648    /// <param name="extraRefs">Optional additional assembly references.</param>
 649    /// <returns>The current <see cref="KestrunHost"/> instance.</returns>
 650    public KestrunHost AddProbe(
 651        string name,
 652        string[]? tags,
 653        string code,
 654        ScriptLanguage? language = null,
 655        IReadOnlyDictionary<string, object?>? arguments = null,
 656        string[]? extraImports = null,
 657        Assembly[]? extraRefs = null)
 658    {
 0659        ArgumentException.ThrowIfNullOrEmpty(name);
 0660        ArgumentException.ThrowIfNullOrEmpty(code);
 661
 0662        var effectiveLanguage = language ?? Options.Health.DefaultScriptLanguage;
 0663        var logger = Logger.ForContext("HealthProbe", name);
 0664        var probe = ScriptProbeFactory.Create(host: this, name: name, tags: tags,
 0665            effectiveLanguage, code: code,
 0666            runspaceAccessor: effectiveLanguage == ScriptLanguage.PowerShell ? () => RunspacePool : null,
 0667            arguments: arguments, extraImports: extraImports, extraRefs: extraRefs);
 668
 0669        RegisterProbeInternal(probe);
 0670        return this;
 671    }
 672
 673    /// <summary>
 674    /// Returns a snapshot of the currently registered probes.
 675    /// </summary>
 676    internal IReadOnlyList<IProbe> GetHealthProbesSnapshot()
 677    {
 0678        lock (_healthProbeLock)
 679        {
 0680            return [.. HealthProbes];
 681        }
 0682    }
 683
 684    private void RegisterProbeInternal(IProbe probe)
 685    {
 56686        lock (_healthProbeLock)
 687        {
 56688            var index = HealthProbes.FindIndex(p => string.Equals(p.Name, probe.Name, StringComparison.OrdinalIgnoreCase
 56689            if (index >= 0)
 690            {
 0691                HealthProbes[index] = probe;
 0692                Logger.Information("Replaced health probe {ProbeName}.", probe.Name);
 693            }
 694            else
 695            {
 56696                HealthProbes.Add(probe);
 56697                Logger.Information("Registered health probe {ProbeName}.", probe.Name);
 698            }
 56699        }
 56700    }
 701
 702    #endregion
 703    #region OpenAPI
 704
 705    /// <summary>
 706    /// Adds callback automation middleware to the Kestrun host.
 707    /// </summary>
 708    /// <param name="options">Optional callback dispatch options.</param>
 709    /// <returns>The updated Kestrun host.</returns>
 710    public KestrunHost AddCallbacksAutomation(CallbackDispatchOptions? options = null)
 711    {
 0712        if (Logger.IsEnabled(LogEventLevel.Debug))
 713        {
 0714            Logger.Debug(
 0715                "Adding callback automation middleware (custom configuration supplied: {HasConfig})",
 0716                options != null);
 717        }
 0718        options ??= new CallbackDispatchOptions();
 0719        if (Logger.IsEnabled(LogEventLevel.Debug))
 720        {
 0721            Logger.Debug("Adding callback automation middleware with options: {@Options}", options);
 722        }
 723
 0724        _ = AddService(services =>
 0725        {
 0726            _ = services.AddSingleton(options ?? new CallbackDispatchOptions());
 0727            _ = services.AddSingleton<InMemoryCallbackQueue>();
 0728            _ = services.AddSingleton<ICallbackDispatcher, InMemoryCallbackDispatcher>();
 0729            _ = services.AddHostedService<InMemoryCallbackDispatchWorker>();
 0730            _ = services.AddHttpClient("kestrun-callbacks", c =>
 0731            {
 0732                c.Timeout = options?.DefaultTimeout ?? TimeSpan.FromSeconds(30);
 0733            });
 0734            _ = services.AddSingleton<ICallbackRetryPolicy>(sp =>
 0735                {
 0736                    return new DefaultCallbackRetryPolicy(options);
 0737                });
 0738
 0739            _ = services.AddSingleton<ICallbackUrlResolver, DefaultCallbackUrlResolver>();
 0740            _ = services.AddSingleton<ICallbackBodySerializer, JsonCallbackBodySerializer>();
 0741
 0742            _ = services.AddHttpClient<ICallbackSender, HttpCallbackSender>();
 0743
 0744            _ = services.AddHostedService<CallbackWorker>();
 0745        });
 0746        return this;
 747    }
 748    #endregion
 749    #region ListenerOptions
 750
 751    /// <summary>
 752    /// Configures a listener for the Kestrun host with the specified port, optional IP address, certificate, protocols,
 753    /// </summary>
 754    /// <param name="port">The port number to listen on.</param>
 755    /// <param name="ipAddress">The IP address to bind to. If null, binds to any address.</param>
 756    /// <param name="x509Certificate">The X509 certificate for HTTPS. If null, HTTPS is not used.</param>
 757    /// <param name="protocols">The HTTP protocols to use.</param>
 758    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 759    /// <returns>The current KestrunHost instance.</returns>
 760    public KestrunHost ConfigureListener(
 761    int port,
 762    IPAddress? ipAddress = null,
 763    X509Certificate2? x509Certificate = null,
 764    HttpProtocols protocols = HttpProtocols.Http1,
 765    bool useConnectionLogging = false)
 766    {
 37767        if (Logger.IsEnabled(LogEventLevel.Debug))
 768        {
 18769            Logger.Debug("ConfigureListener port={Port}, ipAddress={IPAddress}, protocols={Protocols}, useConnectionLogg
 770        }
 771        // Validate state
 37772        if (IsConfigured)
 773        {
 0774            throw new InvalidOperationException("Cannot configure listeners after configuration is applied.");
 775        }
 776        // Validate protocols
 37777        if (protocols == HttpProtocols.Http1AndHttp2AndHttp3 && !CcUtilities.PreviewFeaturesEnabled())
 778        {
 2779            Logger.Warning("Http3 is not supported in this version of Kestrun. Using Http1 and Http2 only.");
 2780            protocols = HttpProtocols.Http1AndHttp2;
 781        }
 782        // Add listener
 37783        Options.Listeners.Add(new ListenerOptions
 37784        {
 37785            IPAddress = ipAddress ?? IPAddress.Any,
 37786            Port = port,
 37787            UseHttps = x509Certificate != null,
 37788            X509Certificate = x509Certificate,
 37789            Protocols = protocols,
 37790            UseConnectionLogging = useConnectionLogging
 37791        });
 37792        return this;
 793    }
 794
 795    /// <summary>
 796    /// Configures a listener for the Kestrun host with the specified port, optional IP address, and connection logging.
 797    /// </summary>
 798    /// <param name="port">The port number to listen on.</param>
 799    /// <param name="ipAddress">The IP address to bind to. If null, binds to any address.</param>
 800    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 801    public void ConfigureListener(
 802    int port,
 803    IPAddress? ipAddress = null,
 20804    bool useConnectionLogging = false) => _ = ConfigureListener(port: port, ipAddress: ipAddress, x509Certificate: null,
 805
 806    /// <summary>
 807    /// Configures a listener for the Kestrun host with the specified port and connection logging option.
 808    /// </summary>
 809    /// <param name="port">The port number to listen on.</param>
 810    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 811    public void ConfigureListener(
 812    int port,
 1813    bool useConnectionLogging = false) => _ = ConfigureListener(port: port, ipAddress: null, x509Certificate: null, prot
 814
 815    /// <summary>
 816    /// Configures listeners for the Kestrun host by resolving the specified host name to IP addresses and binding to ea
 817    /// </summary>
 818    /// <param name="hostName">The host name to resolve and bind to.</param>
 819    /// <param name="port">The port number to listen on.</param>
 820    /// <param name="x509Certificate">The X509 certificate for HTTPS. If null, HTTPS is not used.</param>
 821    /// <param name="protocols">The HTTP protocols to use.</param>
 822    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 823    /// <param name="families">Optional array of address families to filter resolved addresses (e.g., IPv4-only).</param
 824    /// <returns>The current KestrunHost instance.</returns>
 825    /// <exception cref="ArgumentException">Thrown when the host name is null or whitespace.</exception>
 826    /// <exception cref="InvalidOperationException">Thrown when no valid IP addresses are resolved.</exception>
 827    public KestrunHost ConfigureListener(
 828    string hostName,
 829    int port,
 830    X509Certificate2? x509Certificate = null,
 831    HttpProtocols protocols = HttpProtocols.Http1,
 832    bool useConnectionLogging = false,
 833    AddressFamily[]? families = null) // e.g. new[] { AddressFamily.InterNetwork } for IPv4-only
 834    {
 0835        if (string.IsNullOrWhiteSpace(hostName))
 836        {
 0837            throw new ArgumentException("Host name must be provided.", nameof(hostName));
 838        }
 839
 840        // If caller passed an IP literal, just bind once.
 0841        if (IPAddress.TryParse(hostName, out var parsedIp))
 842        {
 0843            _ = ConfigureListener(port, parsedIp, x509Certificate, protocols, useConnectionLogging);
 0844            return this;
 845        }
 846
 847        // Resolve and bind to ALL matching addresses (IPv4/IPv6)
 0848        var addrs = Dns.GetHostAddresses(hostName)
 0849                       .Where(a => families is null || families.Length == 0 || families.Contains(a.AddressFamily))
 0850                       .Where(a => a.AddressFamily is AddressFamily.InterNetwork or AddressFamily.InterNetworkV6)
 0851                       .ToArray();
 852
 0853        if (addrs.Length == 0)
 854        {
 0855            throw new InvalidOperationException($"No IPv4/IPv6 addresses resolved for host '{hostName}'.");
 856        }
 857
 0858        foreach (var addr in addrs)
 859        {
 0860            _ = ConfigureListener(port, addr, x509Certificate, protocols, useConnectionLogging);
 861        }
 862
 0863        return this;
 864    }
 865
 866    /// <summary>
 867    /// Configures listeners for the Kestrun host based on the provided absolute URI, resolving the host to IP addresses
 868    /// </summary>
 869    /// <param name="uri">The absolute URI to configure the listener for.</param>
 870    /// <param name="x509Certificate">The X509 certificate for HTTPS. If null, HTTPS is not used.</param>
 871    /// <param name="protocols">The HTTP protocols to use.</param>
 872    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 873    /// <param name="families">Optional array of address families to filter resolved addresses (e.g., IPv4-only).</param
 874    /// <returns>The current KestrunHost instance.</returns>
 875    /// <exception cref="ArgumentException">Thrown when the provided URI is not absolute.</exception>
 876    /// <exception cref="InvalidOperationException">Thrown when no valid IP addresses are resolved.</exception>
 877    public KestrunHost ConfigureListener(
 878    Uri uri,
 879    X509Certificate2? x509Certificate = null,
 880    HttpProtocols? protocols = null,
 881    bool useConnectionLogging = false,
 882    AddressFamily[]? families = null)
 883    {
 0884        ArgumentNullException.ThrowIfNull(uri);
 885
 0886        if (!uri.IsAbsoluteUri)
 887        {
 0888            throw new ArgumentException("URL must be absolute.", nameof(uri));
 889        }
 890
 0891        var isHttps = uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase);
 0892        var port = uri.IsDefaultPort ? (isHttps ? 443 : 80) : uri.Port;
 893
 894        // Default: HTTPS → H1+H2, HTTP → H1
 0895        var chosenProtocols = protocols ?? (isHttps ? HttpProtocols.Http1AndHttp2 : HttpProtocols.Http1);
 896
 897        // Delegate to hostname overload (which will resolve or handle IP literal)
 0898        return ConfigureListener(
 0899            hostName: uri.Host,
 0900            port: port,
 0901            x509Certificate: x509Certificate,
 0902            protocols: chosenProtocols,
 0903            useConnectionLogging: useConnectionLogging,
 0904            families: families
 0905        );
 906    }
 907
 908    #endregion
 909
 910    #region Configuration
 911
 912    /// <summary>
 913    /// Validates if configuration can be applied and returns early if already configured.
 914    /// </summary>
 915    /// <returns>True if configuration should proceed, false if it should be skipped.</returns>
 916    internal bool ValidateConfiguration()
 917    {
 76918        if (Logger.IsEnabled(LogEventLevel.Debug))
 919        {
 41920            Logger.Debug("EnableConfiguration(options) called");
 921        }
 922
 76923        if (IsConfigured)
 924        {
 18925            if (Logger.IsEnabled(LogEventLevel.Debug))
 926            {
 2927                Logger.Debug("Configuration already applied, skipping");
 928            }
 18929            return false; // Already configured
 930        }
 931
 58932        return true;
 933    }
 934
 935    /// <summary>
 936    /// Creates and initializes the runspace pool for PowerShell execution.
 937    /// </summary>
 938    /// <param name="userVariables">User-defined variables to inject into the runspace pool.</param>
 939    /// <param name="userFunctions">User-defined functions to inject into the runspace pool.</param>
 940    /// <param name="openApiClassesPath">Path to the OpenAPI class definitions to inject into the runspace pool.</param>
 941    /// <exception cref="InvalidOperationException">Thrown when runspace pool creation fails.</exception>
 942    internal void InitializeRunspacePool(Dictionary<string, object>? userVariables, Dictionary<string, string>? userFunc
 943    {
 59944        _runspacePool =
 59945            CreateRunspacePool(Options.MaxRunspaces, userVariables, userFunctions, openApiClassesPath) ??
 59946            throw new InvalidOperationException("Failed to create runspace pool.");
 59947        if (Logger.IsEnabled(LogEventLevel.Verbose))
 948        {
 0949            Logger.Verbose("Runspace pool created with max runspaces: {MaxRunspaces}", Options.MaxRunspaces);
 950        }
 59951    }
 952
 953    /// <summary>
 954    /// Configures the Kestrel web server with basic options.
 955    /// </summary>
 956    internal void ConfigureKestrelBase()
 957    {
 57958        _ = Builder.WebHost.UseKestrel(opts =>
 57959        {
 56960            opts.CopyFromTemplate(Options.ServerOptions);
 113961        });
 57962    }
 963
 964    /// <summary>
 965    /// Configures named pipe listeners if supported on the current platform.
 966    /// </summary>
 967    internal void ConfigureNamedPipes()
 968    {
 58969        if (Options.NamedPipeOptions is not null)
 970        {
 1971            if (OperatingSystem.IsWindows())
 972            {
 0973                _ = Builder.WebHost.UseNamedPipes(opts =>
 0974                {
 0975                    opts.ListenerQueueCount = Options.NamedPipeOptions.ListenerQueueCount;
 0976                    opts.MaxReadBufferSize = Options.NamedPipeOptions.MaxReadBufferSize;
 0977                    opts.MaxWriteBufferSize = Options.NamedPipeOptions.MaxWriteBufferSize;
 0978                    opts.CurrentUserOnly = Options.NamedPipeOptions.CurrentUserOnly;
 0979                    opts.PipeSecurity = Options.NamedPipeOptions.PipeSecurity;
 0980                });
 981            }
 982            else
 983            {
 1984                Logger.Verbose("Named pipe listeners configuration is supported only on Windows; skipping UseNamedPipes 
 985            }
 986        }
 58987    }
 988
 989    /// <summary>
 990    /// Configures HTTPS connection adapter defaults.
 991    /// </summary>
 992    /// <param name="serverOptions">The Kestrel server options to configure.</param>
 993    internal void ConfigureHttpsAdapter(KestrelServerOptions serverOptions)
 994    {
 57995        if (Options.HttpsConnectionAdapter is not null)
 996        {
 0997            Logger.Verbose("Applying HTTPS connection adapter options from KestrunOptions.");
 998
 999            // Apply HTTPS defaults if needed
 01000            serverOptions.ConfigureHttpsDefaults(httpsOptions =>
 01001            {
 01002                httpsOptions.SslProtocols = Options.HttpsConnectionAdapter.SslProtocols;
 01003                httpsOptions.ClientCertificateMode = Options.HttpsConnectionAdapter.ClientCertificateMode;
 01004                httpsOptions.ClientCertificateValidation = Options.HttpsConnectionAdapter.ClientCertificateValidation;
 01005                httpsOptions.CheckCertificateRevocation = Options.HttpsConnectionAdapter.CheckCertificateRevocation;
 01006                httpsOptions.ServerCertificate = Options.HttpsConnectionAdapter.ServerCertificate;
 01007                httpsOptions.ServerCertificateChain = Options.HttpsConnectionAdapter.ServerCertificateChain;
 01008                httpsOptions.ServerCertificateSelector = Options.HttpsConnectionAdapter.ServerCertificateSelector;
 01009                httpsOptions.HandshakeTimeout = Options.HttpsConnectionAdapter.HandshakeTimeout;
 01010                httpsOptions.OnAuthenticate = Options.HttpsConnectionAdapter.OnAuthenticate;
 01011            });
 1012        }
 571013    }
 1014
 1015    /// <summary>
 1016    /// Binds all configured listeners (Unix sockets, named pipes, TCP) to the server.
 1017    /// </summary>
 1018    /// <param name="serverOptions">The Kestrel server options to configure.</param>
 1019    internal void BindListeners(KestrelServerOptions serverOptions)
 1020    {
 1021        // Unix domain socket listeners
 1161022        foreach (var unixSocket in Options.ListenUnixSockets)
 1023        {
 01024            if (!string.IsNullOrWhiteSpace(unixSocket))
 1025            {
 01026                Logger.Verbose("Binding Unix socket: {Sock}", unixSocket);
 01027                serverOptions.ListenUnixSocket(unixSocket);
 1028                // NOTE: control access via directory perms/umask; UDS file perms are inherited from process umask
 1029                // Prefer placing the socket under a group-owned dir (e.g., /var/run/kestrun) with 0770.
 1030            }
 1031        }
 1032
 1033        // Named pipe listeners
 1161034        foreach (var namedPipeName in Options.NamedPipeNames)
 1035        {
 01036            if (!string.IsNullOrWhiteSpace(namedPipeName))
 1037            {
 01038                Logger.Verbose("Binding Named Pipe: {Pipe}", namedPipeName);
 01039                serverOptions.ListenNamedPipe(namedPipeName);
 1040            }
 1041        }
 1042
 1043        // TCP listeners
 1761044        foreach (var opt in Options.Listeners)
 1045        {
 301046            serverOptions.Listen(opt.IPAddress, opt.Port, listenOptions =>
 301047            {
 301048                listenOptions.Protocols = opt.Protocols;
 301049                listenOptions.DisableAltSvcHeader = opt.DisableAltSvcHeader;
 301050                if (opt.UseHttps && opt.X509Certificate is not null)
 301051                {
 21052                    _ = listenOptions.UseHttps(opt.X509Certificate);
 301053                }
 301054                if (opt.UseConnectionLogging)
 301055                {
 01056                    _ = listenOptions.UseConnectionLogging();
 301057                }
 601058            });
 1059        }
 581060    }
 1061
 1062    /// <summary>
 1063    /// Logs the configured endpoints after building the application.
 1064    /// </summary>
 1065    internal void LogConfiguredEndpoints()
 1066    {
 1067        // build the app to validate configuration
 571068        _app = Build();
 1069        // Log configured endpoints
 571070        var dataSource = _app.Services.GetRequiredService<EndpointDataSource>();
 1071
 571072        if (dataSource.Endpoints.Count == 0)
 1073        {
 571074            Logger.Warning("EndpointDataSource is empty. No endpoints configured.");
 1075        }
 1076        else
 1077        {
 01078            foreach (var ep in dataSource.Endpoints)
 1079            {
 01080                Logger.Information("➡️  Endpoint: {DisplayName}", ep.DisplayName);
 1081            }
 1082        }
 01083    }
 1084
 1085    /// <summary>
 1086    /// Handles configuration errors and wraps them with meaningful messages.
 1087    /// </summary>
 1088    /// <param name="ex">The exception that occurred during configuration.</param>
 1089    /// <exception cref="InvalidOperationException">Always thrown with wrapped exception.</exception>
 1090    internal void HandleConfigurationError(Exception ex)
 1091    {
 11092        Logger.Error(ex, "Error applying configuration: {Message}", ex.Message);
 11093        throw new InvalidOperationException("Failed to apply configuration.", ex);
 1094    }
 1095
 1096    /// <summary>
 1097    /// Applies the configured options to the Kestrel server and initializes the runspace pool.
 1098    /// </summary>
 1099    /// <param name="userVariables">User-defined variables to inject into the runspace pool.</param>
 1100    /// <param name="userFunctions">User-defined functions to inject into the runspace pool.</param>
 1101    /// <param name="userCallbacks">User-defined callback functions for OpenAPI classes.</param>
 1102    public void EnableConfiguration(Dictionary<string, object>? userVariables = null, Dictionary<string, string>? userFu
 1103    {
 731104        if (!ValidateConfiguration())
 1105        {
 171106            return;
 1107        }
 1108
 1109        try
 1110        {
 561111            if (Logger.IsEnabled(LogEventLevel.Debug))
 1112            {
 371113                Logger.Debug("Applying configuration to KestrunHost.");
 1114            }
 1115            // Inject user variables into shared state
 561116            _ = ApplyUserVarsToState(userVariables);
 1117
 1118            // Scan for OpenAPI component annotations in the main script.
 1119            // In C#-only scenarios (including xUnit tests), there may be no PowerShell entry script.
 561120            ComponentAnnotations = !string.IsNullOrWhiteSpace(KestrunHostManager.EntryScriptPath)
 561121                && File.Exists(KestrunHostManager.EntryScriptPath)
 561122            ? OpenApiComponentAnnotationScanner.ScanFromPath(mainPath: KestrunHostManager.EntryScriptPath)
 561123            : null;
 1124
 1125            // Export OpenAPI classes from PowerShell
 561126            var openApiClassesPath = ExportOpenApiClasses(userCallbacks);
 1127            // Initialize PowerShell runspace pool
 561128            InitializeRunspacePool(userVariables: null, userFunctions: userFunctions, openApiClassesPath: openApiClasses
 1129            // Configure Kestrel server
 561130            ConfigureKestrelBase();
 1131            // Configure named pipe listeners if any
 561132            ConfigureNamedPipes();
 1133
 1134            // Apply Kestrel listeners and HTTPS settings
 561135            _ = Builder.WebHost.ConfigureKestrel(serverOptions =>
 561136            {
 561137                ConfigureHttpsAdapter(serverOptions);
 561138                BindListeners(serverOptions);
 1121139            });
 1140
 1141            // Generate OpenAPI components after runspace is ready
 1181142            foreach (var openApiDocument in OpenApiDocumentDescriptor.Values)
 1143            {
 31144                openApiDocument.GenerateComponents();
 1145            }
 1146
 1147            // Log configured endpoints after building
 561148            LogConfiguredEndpoints();
 1149
 1150            // Register default probes after endpoints are logged but before marking configured
 561151            RegisterDefaultHealthProbes();
 561152            IsConfigured = true;
 561153            Logger.Information("Configuration applied successfully.");
 561154        }
 01155        catch (Exception ex)
 1156        {
 01157            HandleConfigurationError(ex);
 01158        }
 561159    }
 1160
 1161    /// <summary>
 1162    /// Applies user-defined variables to the shared state.
 1163    /// </summary>
 1164    /// <param name="userVariables">User-defined variables to inject into the shared state.</param>
 1165    /// <returns>True if all variables were successfully applied; otherwise, false.</returns>
 1166    private bool ApplyUserVarsToState(Dictionary<string, object>? userVariables)
 1167    {
 561168        var statusSet = true;
 561169        if (userVariables is not null)
 1170        {
 41171            foreach (var v in userVariables)
 1172            {
 11173                statusSet &= SharedState.Set(v.Key, v.Value, true);
 1174            }
 1175        }
 561176        return statusSet;
 1177    }
 1178
 1179    /// <summary>
 1180    /// Exports OpenAPI classes from PowerShell.
 1181    /// </summary>
 1182    /// <param name="userCallbacks">User-defined callbacks for OpenAPI class export.</param>
 1183    private string ExportOpenApiClasses(Dictionary<string, string>? userCallbacks)
 1184    {
 1185        // Export OpenAPI classes from PowerShell
 561186        var openApiClassesPath = PowerShellOpenApiClassExporter.ExportOpenApiClasses(userCallbacks: userCallbacks);
 561187        if (Logger.IsEnabled(LogEventLevel.Debug))
 1188        {
 371189            if (string.IsNullOrWhiteSpace(openApiClassesPath))
 1190            {
 371191                Logger.Debug("No OpenAPI classes exported from PowerShell.");
 1192            }
 1193            else
 1194            {
 01195                Logger.Debug("Exported OpenAPI classes from PowerShell: {path}", openApiClassesPath);
 1196            }
 1197        }
 561198        return openApiClassesPath;
 1199    }
 1200
 1201    /// <summary>
 1202    /// Registers built-in default health probes (idempotent). Currently includes disk space probe.
 1203    /// </summary>
 1204    private void RegisterDefaultHealthProbes()
 1205    {
 1206        try
 1207        {
 1208            // Avoid duplicate registration if user already added a probe named "disk".
 561209            lock (_healthProbeLock)
 1210            {
 561211                if (HealthProbes.Any(p => string.Equals(p.Name, "disk", StringComparison.OrdinalIgnoreCase)))
 1212                {
 01213                    return; // already present
 1214                }
 561215            }
 1216
 561217            var tags = new[] { IProbe.TAG_SELF }; // neutral tag; user can filter by name if needed
 561218            var diskProbe = new DiskSpaceProbe("disk", tags);
 561219            RegisterProbeInternal(diskProbe);
 561220        }
 01221        catch (Exception ex)
 1222        {
 01223            Logger.Warning(ex, "Failed to register default disk space probe.");
 01224        }
 561225    }
 1226
 1227    #endregion
 1228    #region Builder
 1229    /* More information about the KestrunHost class
 1230    https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication?view=aspnetcore-8.0
 1231
 1232    */
 1233
 1234    /// <summary>
 1235    /// Builds the WebApplication.
 1236    /// This method applies all queued services and middleware stages,
 1237    /// and returns the built WebApplication instance.
 1238    /// </summary>
 1239    /// <returns>The built WebApplication.</returns>
 1240    /// <exception cref="InvalidOperationException"></exception>
 1241    public WebApplication Build()
 1242    {
 1041243        ValidateBuilderState();
 1041244        ApplyQueuedServices();
 1041245        BuildWebApplication();
 1041246        ConfigureBuiltInMiddleware();
 1041247        LogApplicationInfo();
 1041248        ApplyQueuedMiddleware();
 1041249        ApplyFeatures();
 1250
 1041251        return _app!;
 1252    }
 1253
 1254    /// <summary>
 1255    /// Validates that the builder is properly initialized before building.
 1256    /// </summary>
 1257    /// <exception cref="InvalidOperationException">Thrown when the builder is not initialized.</exception>
 1258    private void ValidateBuilderState()
 1259    {
 1041260        if (Builder == null)
 1261        {
 01262            throw new InvalidOperationException("Call CreateBuilder() first.");
 1263        }
 1041264    }
 1265
 1266    /// <summary>
 1267    /// Applies all queued service configurations to the service collection.
 1268    /// </summary>
 1269    private void ApplyQueuedServices()
 1270    {
 3461271        foreach (var configure in _serviceQueue)
 1272        {
 691273            configure(Builder.Services);
 1274        }
 1041275    }
 1276
 1277    /// <summary>
 1278    /// Builds the WebApplication instance from the configured builder.
 1279    /// </summary>
 1280    private void BuildWebApplication()
 1281    {
 1041282        _app = Builder.Build();
 1041283        Logger.Information("Application built successfully.");
 1284
 1285        // 🔔 SignalR shutdown notification
 1041286        _ = _app.Lifetime.ApplicationStopping.Register(() =>
 1041287        {
 1041288            try
 1041289            {
 181290                using var scope = _app.Services.CreateScope();
 1041291
 181292                var isService = scope.ServiceProvider.GetService<IServiceProviderIsService>();
 181293                if (isService?.IsService(typeof(IHubContext<SignalR.KestrunHub>)) != true)
 1041294                {
 181295                    Logger.Debug("SignalR hub context not available. Skipping shutdown notification.");
 181296                    return;
 1041297                }
 1041298
 01299                var hub = scope.ServiceProvider.GetRequiredService<IHubContext<SignalR.KestrunHub>>();
 01300                _ = hub.Clients.All.SendAsync("serverShutdown", "Server stopping");
 01301                Logger.Information("Sent SignalR shutdown notification to clients.");
 01302            }
 01303            catch (Exception ex)
 1041304            {
 01305                Logger.Debug(ex, "Failed to send SignalR shutdown notification.");
 01306            }
 1221307        });
 1041308    }
 1309
 1310    /// <summary>
 1311    /// Configures built-in middleware components in the correct order.
 1312    /// </summary>
 1313    private void ConfigureBuiltInMiddleware()
 1314    {
 1315        // Configure routing
 1041316        ConfigureRouting();
 1317        // Configure CORS
 1041318        ConfigureCors();
 1319        // Configure exception handling
 1041320        ConfigureExceptionHandling();
 1321        // Configure forwarded headers
 1041322        ConfigureForwardedHeaders();
 1323        // Configure status code pages
 1041324        ConfigureStatusCodePages();
 1325        // Configure PowerShell runtime
 1041326        ConfigurePowerShellRuntime();
 1041327    }
 1328
 1329    /// <summary>
 1330    /// Configures routing middleware.
 1331    /// </summary>
 1332    private void ConfigureRouting()
 1333    {
 1041334        if (Logger.IsEnabled(LogEventLevel.Debug))
 1335        {
 821336            Logger.Debug("Enabling routing middleware.");
 1337        }
 1041338        _ = _app!.UseRouting();
 1041339        if (Logger.IsEnabled(LogEventLevel.Debug))
 1340        {
 821341            Logger.Debug("Routing middleware is enabled.");
 1342        }
 1041343    }
 1344
 1345    /// <summary>
 1346    /// Configures CORS middleware if a CORS policy is defined.
 1347    /// </summary>
 1348    private void ConfigureCors()
 1349    {
 1041350        if (CorsPolicyDefined)
 1351        {
 01352            if (Logger.IsEnabled(LogEventLevel.Debug))
 1353            {
 01354                Logger.Debug("Enabling CORS middleware.");
 1355            }
 01356            _ = _app!.UseCors();
 01357            if (Logger.IsEnabled(LogEventLevel.Debug))
 1358            {
 01359                Logger.Debug("CORS middleware is enabled.");
 1360            }
 1361        }
 1041362    }
 1363
 1364    /// <summary>
 1365    /// Configures exception handling middleware if enabled.
 1366    /// </summary>
 1367    private void ConfigureExceptionHandling()
 1368    {
 1041369        if (ExceptionOptions is not null)
 1370        {
 51371            if (Logger.IsEnabled(LogEventLevel.Debug))
 1372            {
 01373                Logger.Debug("Enabling exception handling middleware.");
 1374            }
 51375            _ = ExceptionOptions.DeveloperExceptionPageOptions is not null
 51376                ? _app!.UseDeveloperExceptionPage(ExceptionOptions.DeveloperExceptionPageOptions)
 51377                : _app!.UseExceptionHandler(ExceptionOptions);
 51378            if (Logger.IsEnabled(LogEventLevel.Debug))
 1379            {
 01380                Logger.Debug("Exception handling middleware is enabled.");
 1381            }
 1382        }
 1041383    }
 1384
 1385    /// <summary>
 1386    /// Configures forwarded headers middleware if enabled.
 1387    /// </summary>
 1388    private void ConfigureForwardedHeaders()
 1389    {
 1041390        if (ForwardedHeaderOptions is not null)
 1391        {
 31392            if (Logger.IsEnabled(LogEventLevel.Debug))
 1393            {
 01394                Logger.Debug("Enabling forwarded headers middleware.");
 1395            }
 31396            _ = _app!.UseForwardedHeaders(ForwardedHeaderOptions);
 31397            if (Logger.IsEnabled(LogEventLevel.Debug))
 1398            {
 01399                Logger.Debug("Forwarded headers middleware is enabled.");
 1400            }
 1401        }
 1041402    }
 1403
 1404    /// <summary>
 1405    /// Configures status code pages middleware if enabled.
 1406    /// </summary>
 1407    private void ConfigureStatusCodePages()
 1408    {
 1409        // Register StatusCodePages BEFORE language runtimes so that re-executed requests
 1410        // pass through language middleware again (and get fresh RouteValues/context).
 1041411        if (StatusCodeOptions is not null)
 1412        {
 01413            if (Logger.IsEnabled(LogEventLevel.Debug))
 1414            {
 01415                Logger.Debug("Enabling status code pages middleware.");
 1416            }
 01417            _ = _app!.UseStatusCodePages(StatusCodeOptions);
 01418            if (Logger.IsEnabled(LogEventLevel.Debug))
 1419            {
 01420                Logger.Debug("Status code pages middleware is enabled.");
 1421            }
 1422        }
 1041423    }
 1424
 1425    /// <summary>
 1426    /// Configures PowerShell runtime middleware if enabled.
 1427    /// </summary>
 1428    /// <exception cref="InvalidOperationException">Thrown when PowerShell is enabled but runspace pool is not initializ
 1429    private void ConfigurePowerShellRuntime()
 1430    {
 1041431        if (PowershellMiddlewareEnabled)
 1432        {
 01433            if (Logger.IsEnabled(LogEventLevel.Debug))
 1434            {
 01435                Logger.Debug("Enabling PowerShell middleware.");
 1436            }
 1437
 01438            if (_runspacePool is null)
 1439            {
 01440                throw new InvalidOperationException("Runspace pool is not initialized. Call EnableConfiguration first.")
 1441            }
 1442
 01443            Logger.Information("Adding PowerShell runtime");
 01444            _ = _app!.UseLanguageRuntime(
 01445                    ScriptLanguage.PowerShell,
 01446                    b => b.UsePowerShellRunspace(_runspacePool));
 1447
 01448            if (Logger.IsEnabled(LogEventLevel.Debug))
 1449            {
 01450                Logger.Debug("PowerShell middleware is enabled.");
 1451            }
 1452        }
 1041453    }
 1454
 1455    /// <summary>
 1456    /// Logs application information including working directory and Pages directory contents.
 1457    /// </summary>
 1458    private void LogApplicationInfo()
 1459    {
 1041460        Logger.Information("CWD: {CWD}", GetSafeCurrentDirectory());
 1041461        Logger.Information("ContentRoot: {Root}", _app!.Environment.ContentRootPath);
 1041462        LogPagesDirectory();
 1041463    }
 1464
 1465    /// <summary>
 1466    /// Logs information about the Pages directory and its contents.
 1467    /// </summary>
 1468    private void LogPagesDirectory()
 1469    {
 1041470        var pagesDir = Path.Combine(_app!.Environment.ContentRootPath, "Pages");
 1041471        Logger.Information("Pages Dir: {PagesDir}", pagesDir);
 1472
 1041473        if (Directory.Exists(pagesDir))
 1474        {
 21475            foreach (var file in Directory.GetFiles(pagesDir, "*.*", SearchOption.AllDirectories))
 1476            {
 01477                Logger.Information("Pages file: {File}", file);
 1478            }
 1479        }
 1480        else
 1481        {
 1031482            Logger.Warning("Pages directory does not exist: {PagesDir}", pagesDir);
 1483        }
 1031484    }
 1485
 1486    /// <summary>
 1487    /// Applies all queued middleware stages to the application pipeline.
 1488    /// </summary>
 1489    private void ApplyQueuedMiddleware()
 1490    {
 2941491        foreach (var stage in _middlewareQueue)
 1492        {
 431493            stage(_app!);
 1494        }
 1041495    }
 1496
 1497    /// <summary>
 1498    /// Applies all queued features to the host.
 1499    /// </summary>
 1500    private void ApplyFeatures()
 1501    {
 2121502        foreach (var feature in FeatureQueue)
 1503        {
 21504            feature(this);
 1505        }
 1041506    }
 1507
 1508    /// <summary>
 1509    /// Returns true if the specified service type has already been registered in the IServiceCollection.
 1510    /// </summary>
 1511    public bool IsServiceRegistered(Type serviceType)
 7981512        => Builder?.Services?.Any(sd => sd.ServiceType == serviceType) ?? false;
 1513
 1514    /// <summary>
 1515    /// Generic convenience overload.
 1516    /// </summary>
 01517    public bool IsServiceRegistered<TService>() => IsServiceRegistered(typeof(TService));
 1518
 1519    /// <summary>
 1520    /// Adds a service configuration action to the service queue.
 1521    /// This action will be executed when the services are built.
 1522    /// </summary>
 1523    /// <param name="configure">The service configuration action.</param>
 1524    /// <returns>The current KestrunHost instance.</returns>
 1525    public KestrunHost AddService(Action<IServiceCollection> configure)
 1526    {
 1321527        _serviceQueue.Add(configure);
 1321528        return this;
 1529    }
 1530
 1531    /// <summary>
 1532    /// Adds a middleware stage to the application pipeline.
 1533    /// </summary>
 1534    /// <param name="stage">The middleware stage to add.</param>
 1535    /// <returns>The current KestrunHost instance.</returns>
 1536    public KestrunHost Use(Action<IApplicationBuilder> stage)
 1537    {
 1051538        _middlewareQueue.Add(stage);
 1051539        return this;
 1540    }
 1541
 1542    /// <summary>
 1543    /// Adds a feature configuration action to the feature queue.
 1544    /// This action will be executed when the features are applied.
 1545    /// </summary>
 1546    /// <param name="feature">The feature configuration action.</param>
 1547    /// <returns>The current KestrunHost instance.</returns>
 1548    public KestrunHost AddFeature(Action<KestrunHost> feature)
 1549    {
 21550        FeatureQueue.Add(feature);
 21551        return this;
 1552    }
 1553
 1554    /// <summary>
 1555    /// Adds a scheduling feature to the Kestrun host, optionally specifying the maximum number of runspaces for the sch
 1556    /// </summary>
 1557    /// <param name="MaxRunspaces">The maximum number of runspaces for the scheduler. If null, uses the default value.</
 1558    /// <returns>The current KestrunHost instance.</returns>
 1559    public KestrunHost AddScheduling(int? MaxRunspaces = null)
 1560    {
 41561        return MaxRunspaces is not null and <= 0
 41562            ? throw new ArgumentOutOfRangeException(nameof(MaxRunspaces), "MaxRunspaces must be greater than zero.")
 41563            : AddFeature(host =>
 41564        {
 21565            if (Logger.IsEnabled(LogEventLevel.Debug))
 41566            {
 21567                Logger.Debug("AddScheduling (deferred)");
 41568            }
 41569
 21570            if (host._scheduler is null)
 41571            {
 11572                if (MaxRunspaces is not null and > 0)
 41573                {
 11574                    Logger.Information("Setting MaxSchedulerRunspaces to {MaxRunspaces}", MaxRunspaces);
 11575                    host.Options.MaxSchedulerRunspaces = MaxRunspaces.Value;
 41576                }
 11577                Logger.Verbose("Creating SchedulerService with MaxSchedulerRunspaces={MaxRunspaces}",
 11578                    host.Options.MaxSchedulerRunspaces);
 11579                var pool = host.CreateRunspacePool(host.Options.MaxSchedulerRunspaces);
 11580                var logger = Logger.ForContext<KestrunHost>();
 11581                host.Scheduler = new SchedulerService(pool, logger);
 41582            }
 41583            else
 41584            {
 11585                Logger.Warning("SchedulerService already configured; skipping.");
 41586            }
 51587        });
 1588    }
 1589
 1590    /// <summary>
 1591    /// Adds the Tasks feature to run ad-hoc scripts with status/result/cancellation.
 1592    /// </summary>
 1593    /// <param name="MaxRunspaces">Optional max runspaces for the task PowerShell pool; when null uses scheduler default
 1594    /// <returns>The current KestrunHost instance.</returns>
 1595    public KestrunHost AddTasks(int? MaxRunspaces = null)
 1596    {
 01597        return MaxRunspaces is not null and <= 0
 01598            ? throw new ArgumentOutOfRangeException(nameof(MaxRunspaces), "MaxRunspaces must be greater than zero.")
 01599            : AddFeature(host =>
 01600        {
 01601            if (Logger.IsEnabled(LogEventLevel.Debug))
 01602            {
 01603                Logger.Debug("AddTasks (deferred)");
 01604            }
 01605
 01606            if (host._tasks is null)
 01607            {
 01608                // Reuse scheduler pool sizing unless explicitly overridden
 01609                if (MaxRunspaces is not null and > 0)
 01610                {
 01611                    Logger.Information("Setting MaxTaskRunspaces to {MaxRunspaces}", MaxRunspaces);
 01612                }
 01613                var pool = host.CreateRunspacePool(MaxRunspaces ?? host.Options.MaxSchedulerRunspaces);
 01614                var logger = Logger.ForContext<KestrunHost>();
 01615                host.Tasks = new KestrunTaskService(pool, logger);
 01616            }
 01617            else
 01618            {
 01619                Logger.Warning("KestrunTaskService already configured; skipping.");
 01620            }
 01621        });
 1622    }
 1623
 1624    /// <summary>
 1625    /// Adds MVC / API controllers to the application.
 1626    /// </summary>
 1627    /// <param name="cfg">The configuration options for MVC / API controllers.</param>
 1628    /// <returns>The current KestrunHost instance.</returns>
 1629    public KestrunHost AddControllers(Action<Microsoft.AspNetCore.Mvc.MvcOptions>? cfg = null)
 1630    {
 01631        return AddService(services =>
 01632        {
 01633            var builder = services.AddControllers();
 01634            if (cfg != null)
 01635            {
 01636                _ = builder.ConfigureApplicationPartManager(pm => { }); // customise if you wish
 01637            }
 01638        });
 1639    }
 1640
 1641    /// <summary>
 1642    /// Adds a PowerShell runtime to the application.
 1643    /// This middleware allows you to execute PowerShell scripts in response to HTTP requests.
 1644    /// </summary>
 1645    /// <param name="routePrefix">The route prefix to use for the PowerShell runtime.</param>
 1646    /// <returns>The current KestrunHost instance.</returns>
 1647    public KestrunHost AddPowerShellRuntime(PathString? routePrefix = null)
 1648    {
 11649        if (Logger.IsEnabled(LogEventLevel.Debug))
 1650        {
 11651            Logger.Debug("Adding PowerShell runtime with route prefix: {RoutePrefix}", routePrefix);
 1652        }
 1653
 11654        return Use(app =>
 11655        {
 11656            ArgumentNullException.ThrowIfNull(_runspacePool);
 11657            // ── mount PowerShell at the root ──
 11658            _ = app.UseLanguageRuntime(
 11659                ScriptLanguage.PowerShell,
 21660                b => b.UsePowerShellRunspace(_runspacePool));
 21661        });
 1662    }
 1663
 1664    /// <summary>
 1665    /// Adds the Realtime tag to the OpenAPI document if not already present.
 1666    /// </summary>
 1667    /// <param name="defTag"> OpenAPI document descriptor to which the Realtime tag will be added.</param>
 1668    private static void AddRealTimeTag(OpenApiDocDescriptor defTag)
 1669    {
 1670        // Add Realtime default tag if not present
 21671        if (!defTag.ContainsTag("Realtime"))
 1672        {
 21673            _ = defTag.AddTag(name: "Realtime",
 21674                summary: "Real-time communication",
 21675                description: "Protocols and endpoints for real-time, push-based communication such as SignalR and Server
 21676                kind: "nav",
 21677                externalDocs: new OpenApiExternalDocs
 21678                {
 21679                    Description = "Real-time communication overview",
 21680                    Url = new Uri("https://learn.microsoft.com/aspnet/core/signalr/")
 21681                });
 1682        }
 21683    }
 1684
 1685    /// <summary>
 1686    /// Adds the SignalR tag to the OpenAPI document if not already present.
 1687    /// </summary>
 1688    /// <param name="defTag"> OpenAPI document descriptor to which the SignalR tag will be added.</param>
 1689    private static void AddSignalRTag(OpenApiDocDescriptor defTag)
 1690    {
 01691        if (!defTag.ContainsTag(SignalROptions.DefaultTag))
 1692        {
 01693            _ = defTag.AddTag(name: SignalROptions.DefaultTag,
 01694                 description: "SignalR hubs providing real-time, bidirectional communication over persistent connections
 01695                 summary: "SignalR hubs",
 01696                 parent: "Realtime",
 01697                  externalDocs: new OpenApiExternalDocs
 01698                  {
 01699                      Description = "ASP.NET Core SignalR documentation",
 01700                      Url = new Uri("https://learn.microsoft.com/aspnet/core/signalr/introduction")
 01701                  });
 1702        }
 01703    }
 1704
 1705    /// <summary>
 1706    /// Computes the SignalR negotiate endpoint path based on the hub path.
 1707    /// </summary>
 1708    /// <param name="hubPath">The hub route path.</param>
 1709    /// <returns>The negotiate path for the hub.</returns>
 1710    private static string GetSignalRNegotiatePath(string hubPath)
 01711        => hubPath.EndsWith("/negotiate", StringComparison.OrdinalIgnoreCase)
 01712            ? hubPath
 01713            : hubPath.TrimEnd('/') + "/negotiate";
 1714
 1715    /// <summary>
 1716    /// Creates a native route registration with no script body.
 1717    /// </summary>
 1718    /// <param name="pattern">The route pattern.</param>
 1719    /// <param name="verb">The HTTP verb for the route.</param>
 1720    /// <returns>A configured <see cref="MapRouteOptions"/> instance.</returns>
 1721    private static MapRouteOptions CreateNativeRouteOptions(string pattern, HttpVerb verb)
 01722        => new()
 01723        {
 01724            Pattern = pattern,
 01725            HttpVerbs = [verb],
 01726            ScriptCode = new LanguageOptions
 01727            {
 01728                Language = ScriptLanguage.Native,
 01729                Code = string.Empty
 01730            }
 01731        };
 1732
 1733    /// <summary>
 1734    /// Registers a route in the internal route registry.
 1735    /// </summary>
 1736    /// <param name="pattern">The route pattern.</param>
 1737    /// <param name="verb">The HTTP verb.</param>
 1738    /// <param name="routeOptions">The route options.</param>
 1739    private void RegisterRoute(string pattern, HttpVerb verb, MapRouteOptions routeOptions)
 01740        => _registeredRoutes[(pattern, verb)] = routeOptions;
 1741
 1742    /// <summary>
 1743    /// Ensures the default OpenAPI tags for real-time and SignalR are present when the caller uses default tagging.
 1744    /// </summary>
 1745    /// <param name="options">SignalR configuration options.</param>
 1746    /// <param name="apiDocDescriptors">OpenAPI document descriptors to update.</param>
 1747    private static void EnsureDefaultSignalRTags(SignalROptions options, IEnumerable<OpenApiDocDescriptor> apiDocDescrip
 1748    {
 01749        if (options.Tags?.Contains(SignalROptions.DefaultTag) != true)
 1750        {
 01751            return;
 1752        }
 1753
 01754        foreach (var defTag in apiDocDescriptors)
 1755        {
 01756            AddRealTimeTag(defTag);
 01757            AddSignalRTag(defTag);
 1758        }
 01759    }
 1760
 1761    /// <summary>
 1762    /// Creates the common OpenAPI response set for the SignalR hub connect endpoint.
 1763    /// </summary>
 1764    /// <returns>The OpenAPI responses collection.</returns>
 1765    private static OpenApiResponses CreateSignalRHubResponses()
 01766        => new()
 01767        {
 01768            ["101"] = new OpenApiResponse { Description = "Switching Protocols (WebSocket upgrade)" },
 01769            ["401"] = new OpenApiResponse { Description = "Unauthorized" },
 01770            ["403"] = new OpenApiResponse { Description = "Forbidden" },
 01771            ["404"] = new OpenApiResponse { Description = "Not Found" },
 01772            ["500"] = new OpenApiResponse { Description = "Internal Server Error" }
 01773        };
 1774
 1775    /// <summary>
 1776    /// Creates the common OpenAPI response set for the SignalR negotiate endpoint.
 1777    /// </summary>
 1778    /// <returns>The OpenAPI responses collection.</returns>
 1779    private static OpenApiResponses CreateSignalRNegotiateResponses()
 01780        => new()
 01781        {
 01782            ["200"] = new OpenApiResponse { Description = "Successful negotiation" },
 01783            ["401"] = new OpenApiResponse { Description = "Unauthorized" },
 01784            ["403"] = new OpenApiResponse { Description = "Forbidden" },
 01785            ["404"] = new OpenApiResponse { Description = "Not Found" },
 01786            ["500"] = new OpenApiResponse { Description = "Internal Server Error" }
 01787        };
 1788
 1789    /// <summary>
 1790    /// Builds the OpenAPI extensions for SignalR endpoints.
 1791    /// </summary>
 1792    /// <param name="options">SignalR configuration options.</param>
 1793    /// <param name="negotiatePath">The negotiate endpoint path.</param>
 1794    /// <param name="role">The SignalR endpoint role (e.g., connect, negotiate).</param>
 1795    /// <returns>Extensions dictionary for OpenAPI metadata.</returns>
 1796    private static Dictionary<string, IOpenApiExtension> CreateSignalRExtensions(SignalROptions options, string negotiat
 01797        => new()
 01798        {
 01799            ["x-signalr-role"] = new JsonNodeExtension(JsonValue.Create(role)),
 01800            ["x-signalr"] = new JsonNodeExtension(new JsonObject
 01801            {
 01802                ["hub"] = options.HubName,
 01803                ["path"] = options.Path,
 01804                ["negotiatePath"] = negotiatePath,
 01805                ["connectOperation"] = "get:" + options.Path,
 01806                ["transports"] = new JsonArray("websocket", "sse", "longPolling"),
 01807                ["formats"] = new JsonArray("json"),
 01808            })
 01809        };
 1810
 1811    /// <summary>
 1812    /// Adds OpenAPI metadata to the hub connect route, if OpenAPI is enabled.
 1813    /// </summary>
 1814    /// <param name="options">SignalR configuration options.</param>
 1815    /// <param name="apiDocDescriptors">OpenAPI document descriptors for tag registration.</param>
 1816    /// <param name="routeOptions">The route options to enrich with OpenAPI metadata.</param>
 1817    /// <param name="negotiatePath">The computed negotiate endpoint path.</param>
 1818    private void TryAddSignalRHubOpenApiMetadata(
 1819        SignalROptions options,
 1820        IEnumerable<OpenApiDocDescriptor> apiDocDescriptors,
 1821        MapRouteOptions routeOptions,
 1822        string negotiatePath)
 1823    {
 01824        if (options.SkipOpenApi)
 1825        {
 01826            return;
 1827        }
 1828
 01829        if (Logger.IsEnabled(LogEventLevel.Debug))
 1830        {
 01831            Logger.Debug("Adding OpenAPI metadata for SignalR hub at path: {Path}", options.Path);
 1832        }
 1833
 01834        EnsureDefaultSignalRTags(options, apiDocDescriptors);
 1835
 01836        var meta = new OpenAPIPathMetadata(pattern: options.Path, mapOptions: routeOptions)
 01837        {
 01838            DocumentId = options.DocId,
 01839            Summary = string.IsNullOrWhiteSpace(options.Summary) ? null : options.Summary,
 01840            Description = string.IsNullOrWhiteSpace(options.Description) ? null : options.Description,
 01841            Tags = options.Tags?.ToList() ?? [],
 01842            Responses = CreateSignalRHubResponses(),
 01843            Extensions = CreateSignalRExtensions(options, negotiatePath, role: "connect")
 01844        };
 1845
 01846        routeOptions.OpenAPI[HttpVerb.Get] = meta;
 01847    }
 1848
 1849    /// <summary>
 1850    /// Adds OpenAPI metadata to the negotiate route, if OpenAPI is enabled.
 1851    /// </summary>
 1852    /// <param name="options">SignalR configuration options.</param>
 1853    /// <param name="negotiateRouteOptions">The negotiate route options to enrich with OpenAPI metadata.</param>
 1854    /// <param name="negotiatePath">The negotiate endpoint path.</param>
 1855    private static void TryAddSignalRNegotiateOpenApiMetadata(
 1856        SignalROptions options,
 1857        MapRouteOptions negotiateRouteOptions,
 1858        string negotiatePath)
 1859    {
 01860        if (options.SkipOpenApi)
 1861        {
 01862            return;
 1863        }
 1864
 01865        var negotiateMeta = new OpenAPIPathMetadata(pattern: negotiatePath, mapOptions: negotiateRouteOptions)
 01866        {
 01867            Summary = "SignalR negotiate endpoint",
 01868            Description = "Negotiates connection parameters for a SignalR client before establishing the transport.",
 01869            Tags = options.Tags?.ToList() ?? [],
 01870            Responses = CreateSignalRNegotiateResponses(),
 01871            Extensions = CreateSignalRExtensions(options, negotiatePath, role: "negotiate")
 01872        };
 1873
 01874        negotiateRouteOptions.OpenAPI[HttpVerb.Post] = negotiateMeta;
 01875    }
 1876
 1877    /// <summary>
 1878    /// Registers SignalR services and JSON protocol configuration.
 1879    /// </summary>
 1880    /// <typeparam name="THub">The hub type being registered.</typeparam>
 1881    /// <param name="services">The service collection to configure.</param>
 1882    private static void ConfigureSignalRServices<THub>(IServiceCollection services) where THub : Hub
 1883    {
 01884        _ = services.AddSignalR(o =>
 01885        {
 01886            o.HandshakeTimeout = TimeSpan.FromSeconds(5);
 01887            o.KeepAliveInterval = TimeSpan.FromSeconds(2);
 01888            o.ClientTimeoutInterval = TimeSpan.FromSeconds(10);
 01889        }).AddJsonProtocol(opts =>
 01890        {
 01891            // Avoid failures when payloads contain cycles; our sanitizer should prevent most, this is a safety net.
 01892            opts.PayloadSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
 01893        });
 1894
 1895        // Register IRealtimeBroadcaster as singleton if it's the KestrunHub
 01896        if (typeof(THub) == typeof(SignalR.KestrunHub))
 1897        {
 01898            _ = services.AddSingleton<SignalR.IRealtimeBroadcaster, SignalR.RealtimeBroadcaster>();
 01899            _ = services.AddSingleton<SignalR.IConnectionTracker, SignalR.InMemoryConnectionTracker>();
 1900        }
 01901    }
 1902
 1903    /// <summary>
 1904    /// Maps the SignalR hub to the application's endpoint route builder.
 1905    /// </summary>
 1906    /// <typeparam name="THub">The hub type being mapped.</typeparam>
 1907    /// <param name="app">The application builder.</param>
 1908    /// <param name="path">The hub path.</param>
 1909    private static void MapSignalRHub<THub>(IApplicationBuilder app, string path) where THub : Hub
 01910        => ((IEndpointRouteBuilder)app).MapHub<THub>(path);
 1911
 1912    /// <summary>
 1913    /// Adds a SignalR hub to the application at the specified path.
 1914    /// </summary>
 1915    /// <typeparam name="T">The type of the SignalR hub.</typeparam>
 1916    /// <param name="options">The options for configuring the SignalR hub.</param>
 1917    /// <returns>The current KestrunHost instance.</returns>
 1918    public KestrunHost AddSignalR<T>(SignalROptions options) where T : Hub
 1919    {
 01920        options ??= SignalROptions.Default;
 1921
 01922        var apiDocDescriptors = GetOrCreateOpenApiDocument(options.DocId);
 01923        var negotiatePath = GetSignalRNegotiatePath(options.Path);
 1924
 01925        var routeOptions = CreateNativeRouteOptions(options.Path, HttpVerb.Get);
 01926        TryAddSignalRHubOpenApiMetadata(options, apiDocDescriptors, routeOptions, negotiatePath);
 01927        RegisterRoute(options.Path, HttpVerb.Get, routeOptions);
 1928
 01929        if (options.IncludeNegotiateEndpoint)
 1930        {
 01931            var negotiateRouteOptions = CreateNativeRouteOptions(negotiatePath, HttpVerb.Post);
 01932            TryAddSignalRNegotiateOpenApiMetadata(options, negotiateRouteOptions, negotiatePath);
 01933            RegisterRoute(negotiatePath, HttpVerb.Post, negotiateRouteOptions);
 1934        }
 1935
 01936        if (Logger.IsEnabled(LogEventLevel.Debug))
 1937        {
 01938            Logger.Debug("Adding SignalR hub of type {HubType} at path: {Path}", typeof(T).FullName, options.Path);
 1939        }
 1940
 01941        return AddService(ConfigureSignalRServices<T>)
 01942            .Use(app => MapSignalRHub<T>(app, options.Path));
 1943    }
 1944
 1945    /// <summary>
 1946    /// Adds the default SignalR hub (KestrunHub) to the application at the specified path.
 1947    /// </summary>
 1948    /// <param name="options">The options for configuring the SignalR hub.</param>
 1949    /// <returns></returns>
 01950    public KestrunHost AddSignalR(SignalROptions options) => AddSignalR<SignalR.KestrunHub>(options);
 1951
 1952    /*
 1953        // ④ gRPC
 1954        public KestrunHost AddGrpc<TService>() where TService : class
 1955        {
 1956            return AddService(s => s.AddGrpc())
 1957                   .Use(app => app.MapGrpcService<TService>());
 1958        }
 1959    */
 1960
 1961    // Add as many tiny helpers as you wish:
 1962    // • AddAuthentication(jwt => { … })
 1963    // • AddSignalR()
 1964    // • AddHealthChecks()
 1965    // • AddGrpc()
 1966    // etc.
 1967
 1968    #endregion
 1969    #region Run/Start/Stop
 1970
 1971    /// <summary>
 1972    /// Runs the Kestrun web application, applying configuration and starting the server.
 1973    /// </summary>
 1974    public void Run()
 1975    {
 01976        if (Logger.IsEnabled(LogEventLevel.Debug))
 1977        {
 01978            Logger.Debug("Run() called");
 1979        }
 1980
 01981        EnableConfiguration();
 01982        StartTime = DateTime.UtcNow;
 01983        _app?.Run();
 01984    }
 1985
 1986    /// <summary>
 1987    /// Starts the Kestrun web application asynchronously.
 1988    /// </summary>
 1989    /// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
 1990    /// <returns>A task that represents the asynchronous start operation.</returns>
 1991    public async Task StartAsync(CancellationToken cancellationToken = default)
 1992    {
 171993        if (Logger.IsEnabled(LogEventLevel.Debug))
 1994        {
 11995            Logger.Debug("StartAsync() called");
 1996        }
 1997
 171998        EnableConfiguration();
 171999        if (_app != null)
 2000        {
 172001            StartTime = DateTime.UtcNow;
 172002            await _app.StartAsync(cancellationToken);
 2003        }
 172004    }
 2005
 2006    /// <summary>
 2007    /// Stops the Kestrun web application asynchronously.
 2008    /// </summary>
 2009    /// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
 2010    /// <returns>A task that represents the asynchronous stop operation.</returns>
 2011    public async Task StopAsync(CancellationToken cancellationToken = default)
 2012    {
 222013        if (Logger.IsEnabled(LogEventLevel.Debug))
 2014        {
 62015            Logger.Debug("StopAsync() called");
 2016        }
 2017
 222018        if (_app != null)
 2019        {
 2020            try
 2021            {
 2022                // Initiate graceful shutdown
 172023                await _app.StopAsync(cancellationToken);
 172024                StopTime = DateTime.UtcNow;
 172025            }
 02026            catch (Exception ex) when (ex.GetType().FullName == "System.Net.Quic.QuicException")
 2027            {
 2028                // QUIC exceptions can occur during shutdown, especially if the server is not using QUIC.
 2029                // We log this as a debug message to avoid cluttering the logs with expected exceptions.
 2030                // This is a workaround for
 2031
 02032                Logger.Debug("Ignored QUIC exception during shutdown: {Message}", ex.Message);
 02033            }
 2034        }
 222035    }
 2036
 2037    /// <summary>
 2038    /// Initiates a graceful shutdown of the Kestrun web application.
 2039    /// </summary>
 2040    public void Stop()
 2041    {
 12042        if (Interlocked.Exchange(ref _stopping, 1) == 1)
 2043        {
 02044            return; // already stopping
 2045        }
 12046        if (Logger.IsEnabled(LogEventLevel.Debug))
 2047        {
 12048            Logger.Debug("Stop() called");
 2049        }
 2050        // This initiates a graceful shutdown.
 12051        _app?.Lifetime.StopApplication();
 12052        StopTime = DateTime.UtcNow;
 12053    }
 2054
 2055    /// <summary>
 2056    /// Determines whether the Kestrun web application is currently running.
 2057    /// </summary>
 2058    /// <returns>True if the application is running; otherwise, false.</returns>
 2059    public bool IsRunning
 2060    {
 2061        get
 2062        {
 82063            var appField = typeof(KestrunHost)
 82064                .GetField("_app", BindingFlags.NonPublic | BindingFlags.Instance);
 2065
 82066            return appField?.GetValue(this) is WebApplication app && !app.Lifetime.ApplicationStopping.IsCancellationReq
 2067        }
 2068    }
 2069
 2070    #endregion
 2071
 2072    #region Runspace Pool Management
 2073
 2074    /// <summary>
 2075    /// Creates and returns a new <see cref="KestrunRunspacePoolManager"/> instance with the specified maximum number of
 2076    /// </summary>
 2077    /// <param name="maxRunspaces">The maximum number of runspaces to create. If not specified or zero, defaults to twic
 2078    /// <param name="userVariables">A dictionary of user-defined variables to inject into the runspace pool.</param>
 2079    /// <param name="userFunctions">A dictionary of user-defined functions to inject into the runspace pool.</param>
 2080    /// <param name="openApiClassesPath">The file path to the OpenAPI class definitions to inject into the runspace pool
 2081    /// <returns>A configured <see cref="KestrunRunspacePoolManager"/> instance.</returns>
 2082    public KestrunRunspacePoolManager CreateRunspacePool(int? maxRunspaces = 0, Dictionary<string, object>? userVariable
 2083    {
 652084        LogCreateRunspacePool(maxRunspaces);
 2085
 652086        var iss = BuildInitialSessionState(openApiClassesPath);
 652087        AddHostVariables(iss);
 652088        AddSharedVariables(iss);
 652089        AddUserVariables(iss, userVariables);
 652090        AddUserFunctions(iss, userFunctions);
 2091
 652092        var maxRs = ResolveMaxRunspaces(maxRunspaces);
 2093
 652094        Logger.Information("Creating runspace pool with max runspaces: {MaxRunspaces}", maxRs);
 652095        return new KestrunRunspacePoolManager(this, Options?.MinRunspaces ?? 1, maxRunspaces: maxRs, initialSessionState
 2096    }
 2097
 2098    private void LogCreateRunspacePool(int? maxRunspaces)
 2099    {
 652100        if (Logger.IsEnabled(LogEventLevel.Debug))
 2101        {
 462102            Logger.Debug("CreateRunspacePool() called: {@MaxRunspaces}", maxRunspaces);
 2103        }
 652104    }
 2105
 2106    private InitialSessionState BuildInitialSessionState(string? openApiClassesPath)
 2107    {
 652108        var iss = InitialSessionState.CreateDefault();
 2109
 652110        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
 2111        {
 2112            // On Windows, we can use the full .NET Framework modules
 02113            iss.ExecutionPolicy = ExecutionPolicy.Unrestricted;
 2114        }
 2115
 652116        ImportModulePaths(iss);
 652117        AddOpenApiStartupScript(iss, openApiClassesPath);
 2118
 652119        return iss;
 2120    }
 2121
 2122    private void ImportModulePaths(InitialSessionState iss)
 2123    {
 2602124        foreach (var path in _modulePaths)
 2125        {
 652126            iss.ImportPSModule([path]);
 2127        }
 652128    }
 2129
 2130    private void AddOpenApiStartupScript(InitialSessionState iss, string? openApiClassesPath)
 2131    {
 652132        if (string.IsNullOrWhiteSpace(openApiClassesPath))
 2133        {
 642134            return;
 2135        }
 2136
 12137        _ = iss.StartupScripts.Add(openApiClassesPath);
 12138        if (Logger.IsEnabled(LogEventLevel.Debug))
 2139        {
 12140            Logger.Debug("Configured OpenAPI class script at {ScriptPath}", openApiClassesPath);
 2141        }
 12142    }
 2143
 2144    private void AddHostVariables(InitialSessionState iss)
 2145    {
 652146        iss.Variables.Add(
 652147            new SessionStateVariableEntry(
 652148                "KrServer",
 652149                this,
 652150                "The Kestrun Server Host (KestrunHost) instance"
 652151            )
 652152        );
 652153    }
 2154
 2155    private void AddSharedVariables(InitialSessionState iss)
 2156    {
 1322157        foreach (var kvp in SharedState.Snapshot())
 2158        {
 12159            iss.Variables.Add(
 12160                new SessionStateVariableEntry(
 12161                    kvp.Key,
 12162                    kvp.Value,
 12163                    "Global variable"
 12164                )
 12165            );
 2166        }
 652167    }
 2168
 2169    private static void AddUserVariables(InitialSessionState iss, IReadOnlyDictionary<string, object>? userVariables)
 2170    {
 652171        if (userVariables is null)
 2172        {
 632173            return;
 2174        }
 2175
 102176        foreach (var kvp in userVariables)
 2177        {
 32178            if (kvp.Value is PSVariable psVar)
 2179            {
 12180                iss.Variables.Add(
 12181                    new SessionStateVariableEntry(
 12182                        kvp.Key,
 12183                        UnwrapKestrunVariableValue(psVar.Value),
 12184                        psVar.Description ?? "User-defined variable"
 12185                    )
 12186                );
 12187                continue;
 2188            }
 2189
 22190            iss.Variables.Add(
 22191                new SessionStateVariableEntry(
 22192                    kvp.Key,
 22193                    UnwrapKestrunVariableValue(kvp.Value),
 22194                    "User-defined variable"
 22195                )
 22196            );
 2197        }
 22198    }
 2199
 2200    /// <summary>
 2201    /// Unwraps a Kestrun variable value if it is wrapped in a dictionary with a specific marker.
 2202    /// </summary>
 2203    /// <param name="raw">The raw variable value to unwrap.</param>
 2204    /// <returns>The unwrapped variable value, or the original value if not wrapped.</returns>
 2205    private static object? UnwrapKestrunVariableValue(object? raw)
 2206    {
 32207        if (raw is null)
 2208        {
 02209            return null;
 2210        }
 2211
 2212        // unwrap PSObject if needed
 32213        raw = UnwrapPsObject(raw);
 2214
 2215        // check for dictionary
 32216        if (raw is not System.Collections.IDictionary dict)
 2217        {
 32218            return raw;
 2219        }
 2220
 2221        // check for marker key
 02222        if (!TryGetDictionaryValueIgnoreCase(dict, KestrunVariableMarkerKey, out var markerObj))
 2223        {
 02224            return raw;
 2225        }
 2226
 2227        // check if marker is enabled
 02228        if (!IsKestrunVariableMarkerEnabled(markerObj))
 2229        {
 02230            return raw;
 2231        }
 2232
 2233        // extract the "Value" entry
 02234        return TryGetDictionaryValueIgnoreCase(dict, "Value", out var valueObj)
 02235            ? UnwrapPsObject(valueObj)
 02236            : null;
 2237    }
 2238
 2239    /// <summary>
 2240    /// Unwraps a PowerShell <see cref="PSObject"/> by returning its <see cref="PSObject.BaseObject"/>.
 2241    /// </summary>
 2242    /// <param name="raw">The value to unwrap.</param>
 2243    /// <returns>The underlying base object when <paramref name="raw"/> is a <see cref="PSObject"/>, otherwise <paramref
 2244    private static object? UnwrapPsObject(object? raw)
 32245        => raw is PSObject pso ? pso.BaseObject : raw;
 2246
 2247    /// <summary>
 2248    /// Determines whether the Kestrun variable marker is enabled.
 2249    /// </summary>
 2250    /// <param name="markerObj">The marker value (typically a boolean or a PowerShell-wrapped boolean).</param>
 2251    /// <returns><c>true</c> if the marker indicates the value is wrapped; otherwise, <c>false</c>.</returns>
 2252    private static bool IsKestrunVariableMarkerEnabled(object? markerObj)
 02253        => markerObj switch
 02254        {
 02255            bool b => b,
 02256            PSObject psMarker when psMarker.BaseObject is bool b => b,
 02257            _ => false
 02258        };
 2259
 2260    private static bool TryGetDictionaryValueIgnoreCase(System.Collections.IDictionary dict, string key, out object? val
 2261    {
 02262        value = null;
 2263
 02264        if (dict.Contains(key))
 2265        {
 02266            value = dict[key];
 02267            return true;
 2268        }
 2269
 02270        foreach (System.Collections.DictionaryEntry de in dict)
 2271        {
 02272            if (de.Key is string s && string.Equals(s, key, StringComparison.OrdinalIgnoreCase))
 2273            {
 02274                value = de.Value;
 02275                return true;
 2276            }
 2277        }
 2278
 02279        return false;
 02280    }
 2281
 2282    private static void AddUserFunctions(InitialSessionState iss, IReadOnlyDictionary<string, string>? userFunctions)
 2283    {
 652284        if (userFunctions is null)
 2285        {
 622286            return;
 2287        }
 2288
 122289        foreach (var function in userFunctions)
 2290        {
 32291            var entry = new SessionStateFunctionEntry(
 32292                function.Key,
 32293                function.Value,
 32294                ScopedItemOptions.ReadOnly,
 32295                helpFile: null
 32296            );
 2297
 32298            iss.Commands.Add(entry);
 2299        }
 32300    }
 2301
 2302    private static int ResolveMaxRunspaces(int? maxRunspaces) =>
 652303        (maxRunspaces.HasValue && maxRunspaces.Value > 0)
 652304            ? maxRunspaces.Value
 652305            : Environment.ProcessorCount * 2;
 2306
 2307    #endregion
 2308
 2309    #region Disposable
 2310
 2311    /// <summary>
 2312    /// Releases all resources used by the <see cref="KestrunHost"/> instance.
 2313    /// </summary>
 2314    public void Dispose()
 2315    {
 2642316        if (Logger.IsEnabled(LogEventLevel.Debug))
 2317        {
 2592318            Logger.Debug("Dispose() called");
 2319        }
 2320
 2642321        _runspacePool?.Dispose();
 2642322        _runspacePool = null; // Clear the runspace pool reference
 2642323        IsConfigured = false; // Reset configuration state
 2642324        _app = null;
 2642325        _scheduler?.Dispose();
 2642326        (Logger as IDisposable)?.Dispose();
 2642327        GC.SuppressFinalize(this);
 2642328    }
 2329    #endregion
 2330
 2331    #region Script Validation
 2332
 2333    #endregion
 2334}

Methods/Properties

AddHealthEndpoint(System.Action`1<Kestrun.Health.HealthEndpointOptions>)
AddHealthEndpoint(Kestrun.Health.HealthEndpointOptions)
ExtractTags(Microsoft.AspNetCore.Http.HttpRequest)
CopyHealthEndpointOptions(Kestrun.Health.HealthEndpointOptions,Kestrun.Health.HealthEndpointOptions)
DetermineStatusCode(Kestrun.Health.ProbeStatus,System.Boolean)
MapHealthEndpointImmediate(Kestrun.Health.HealthEndpointOptions,Kestrun.Hosting.Options.MapRouteOptions)
AddLocalization(System.Action`1<Kestrun.Localization.KestrunLocalizationOptions>)
AddLocalization(Kestrun.Localization.KestrunLocalizationOptions)
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)
AddSseBroadcast(Kestrun.Hosting.Options.SseBroadcastOptions)
AddSseTag(Kestrun.OpenApi.OpenApiDocDescriptor)
RegisterSseBroadcastRouteForOpenApi(Kestrun.Hosting.Options.SseBroadcastOptions)
GetSseConnectedClientCount()
BroadcastSseEventAsync()
HandleSseBroadcastConnectAsync()
PumpSseAsync()
WritePayloadAsync()
AddDefaultFiles(Microsoft.AspNetCore.Builder.DefaultFilesOptions)
AddDefaultFiles(System.Action`1<Microsoft.AspNetCore.Builder.DefaultFilesOptions>)
AddDirectoryBrowser(System.String)
AddFavicon(System.String)
CopyStaticFileOptions(Microsoft.AspNetCore.Builder.StaticFileOptions,Microsoft.AspNetCore.Builder.StaticFileOptions)
CopyDefaultFilesOptions(Microsoft.AspNetCore.Builder.DefaultFilesOptions,Microsoft.AspNetCore.Builder.DefaultFilesOptions)
AddFileServer(Microsoft.AspNetCore.Builder.FileServerOptions,Microsoft.Net.Http.Headers.CacheControlHeaderValue)
AddFileServer(System.Action`1<Microsoft.AspNetCore.Builder.FileServerOptions>)
AddStaticFiles(System.Action`1<Microsoft.AspNetCore.Builder.StaticFileOptions>)
AddStaticFiles(Microsoft.AspNetCore.Builder.StaticFileOptions,Microsoft.Net.Http.Headers.CacheControlHeaderValue)
.cctor()
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_LocalizationStore()
get_DefaultHost()
get_DefinedCorsPolicyNames()
get_CorsPolicyDefined()
get_ComponentAnnotations()
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()
get_OpenApiDocumentIds()
get_DefaultOpenApiDocumentDescriptor()
.ctor(System.String,System.String,System.String[])
.ctor(System.String,Serilog.ILogger,System.Boolean)
CreateWebAppOptions()
GetOrCreateOpenApiDocument(System.String)
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)
AddCallbacksAutomation(Kestrun.Callback.CallbackDispatchOptions)
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>,System.Collections.Generic.Dictionary`2<System.String,System.String>)
ApplyUserVarsToState(System.Collections.Generic.Dictionary`2<System.String,System.Object>)
ExportOpenApiClasses(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>)
AddRealTimeTag(Kestrun.OpenApi.OpenApiDocDescriptor)
AddSignalRTag(Kestrun.OpenApi.OpenApiDocDescriptor)
GetSignalRNegotiatePath(System.String)
CreateNativeRouteOptions(System.String,Kestrun.Utilities.HttpVerb)
RegisterRoute(System.String,Kestrun.Utilities.HttpVerb,Kestrun.Hosting.Options.MapRouteOptions)
EnsureDefaultSignalRTags(Kestrun.Hosting.Options.SignalROptions,System.Collections.Generic.IEnumerable`1<Kestrun.OpenApi.OpenApiDocDescriptor>)
CreateSignalRHubResponses()
CreateSignalRNegotiateResponses()
CreateSignalRExtensions(Kestrun.Hosting.Options.SignalROptions,System.String,System.String)
TryAddSignalRHubOpenApiMetadata(Kestrun.Hosting.Options.SignalROptions,System.Collections.Generic.IEnumerable`1<Kestrun.OpenApi.OpenApiDocDescriptor>,Kestrun.Hosting.Options.MapRouteOptions,System.String)
TryAddSignalRNegotiateOpenApiMetadata(Kestrun.Hosting.Options.SignalROptions,Kestrun.Hosting.Options.MapRouteOptions,System.String)
ConfigureSignalRServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)
MapSignalRHub(Microsoft.AspNetCore.Builder.IApplicationBuilder,System.String)
AddSignalR(Kestrun.Hosting.Options.SignalROptions)
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>)
UnwrapKestrunVariableValue(System.Object)
UnwrapPsObject(System.Object)
IsKestrunVariableMarkerEnabled(System.Object)
TryGetDictionaryValueIgnoreCase(System.Collections.IDictionary,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()