< Summary - Kestrun — Combined Coverage

Line coverage
58%
Covered lines: 886
Uncovered lines: 630
Coverable lines: 1516
Total lines: 3791
Line coverage: 58.4%
Branch coverage
52%
Covered branches: 338
Total branches: 638
Branch coverage: 52.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 09/12/2025 - 13:32:05 Line coverage: 73.8% (257/348) Branch coverage: 71% (108/152) Total lines: 938 Tag: Kestrun/Kestrun@63ea5841fe73fd164406accba17a956e8c08357f09/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@ca54e35c77799b76774b3805b6f075cdbc0c5fbe02/05/2026 - 00:28:18 Line coverage: 59% (875/1482) Branch coverage: 54.4% (341/626) Total lines: 3709 Tag: Kestrun/Kestrun@d9261bd752e45afa789d10bc0c82b7d5724d958902/08/2026 - 19:37:33 Line coverage: 58.5% (868/1482) Branch coverage: 53.3% (334/626) Total lines: 3709 Tag: Kestrun/Kestrun@b575dbec8cb8e37804d6b185d7e4356de13a7cd002/17/2026 - 11:06:21 Line coverage: 59% (875/1482) Branch coverage: 54.4% (341/626) Total lines: 3709 Tag: Kestrun/Kestrun@0fc8ae5922e26e71a6ee994cde19e3653b5aac4b02/18/2026 - 08:33:07 Line coverage: 59.1% (880/1488) Branch coverage: 54.4% (342/628) Total lines: 3730 Tag: Kestrun/Kestrun@bf8a937cfb7e8936c225b9df4608f8ddd85558b102/19/2026 - 11:34:19 Line coverage: 58.6% (873/1488) Branch coverage: 53.3% (335/628) Total lines: 3730 Tag: Kestrun/Kestrun@8aa46e1988031758b311143cd39bf5749fbcd39e02/23/2026 - 14:28:53 Line coverage: 59.1% (880/1488) Branch coverage: 54.4% (342/628) Total lines: 3730 Tag: Kestrun/Kestrun@f2ab0d7f4a797bca3a377d435ed41b560b82ac4903/03/2026 - 12:38:30 Line coverage: 58.6% (873/1488) Branch coverage: 53.3% (335/628) Total lines: 3730 Tag: Kestrun/Kestrun@7602da75ddb0abe534368b9e9ce2f45ae966769a03/03/2026 - 20:14:57 Line coverage: 59.1% (880/1488) Branch coverage: 54.4% (342/628) Total lines: 3730 Tag: Kestrun/Kestrun@d169bd1d1e32ec576bfd2135357b6d01a2ad23ba03/04/2026 - 19:40:34 Line coverage: 58.4% (886/1516) Branch coverage: 52.9% (338/638) Total lines: 3791 Tag: Kestrun/Kestrun@eeafbe813231ed23417e7b339e170e307b2c86f9 09/12/2025 - 13:32:05 Line coverage: 73.8% (257/348) Branch coverage: 71% (108/152) Total lines: 938 Tag: Kestrun/Kestrun@63ea5841fe73fd164406accba17a956e8c08357f09/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@ca54e35c77799b76774b3805b6f075cdbc0c5fbe02/05/2026 - 00:28:18 Line coverage: 59% (875/1482) Branch coverage: 54.4% (341/626) Total lines: 3709 Tag: Kestrun/Kestrun@d9261bd752e45afa789d10bc0c82b7d5724d958902/08/2026 - 19:37:33 Line coverage: 58.5% (868/1482) Branch coverage: 53.3% (334/626) Total lines: 3709 Tag: Kestrun/Kestrun@b575dbec8cb8e37804d6b185d7e4356de13a7cd002/17/2026 - 11:06:21 Line coverage: 59% (875/1482) Branch coverage: 54.4% (341/626) Total lines: 3709 Tag: Kestrun/Kestrun@0fc8ae5922e26e71a6ee994cde19e3653b5aac4b02/18/2026 - 08:33:07 Line coverage: 59.1% (880/1488) Branch coverage: 54.4% (342/628) Total lines: 3730 Tag: Kestrun/Kestrun@bf8a937cfb7e8936c225b9df4608f8ddd85558b102/19/2026 - 11:34:19 Line coverage: 58.6% (873/1488) Branch coverage: 53.3% (335/628) Total lines: 3730 Tag: Kestrun/Kestrun@8aa46e1988031758b311143cd39bf5749fbcd39e02/23/2026 - 14:28:53 Line coverage: 59.1% (880/1488) Branch coverage: 54.4% (342/628) Total lines: 3730 Tag: Kestrun/Kestrun@f2ab0d7f4a797bca3a377d435ed41b560b82ac4903/03/2026 - 12:38:30 Line coverage: 58.6% (873/1488) Branch coverage: 53.3% (335/628) Total lines: 3730 Tag: Kestrun/Kestrun@7602da75ddb0abe534368b9e9ce2f45ae966769a03/03/2026 - 20:14:57 Line coverage: 59.1% (880/1488) Branch coverage: 54.4% (342/628) Total lines: 3730 Tag: Kestrun/Kestrun@d169bd1d1e32ec576bfd2135357b6d01a2ad23ba03/04/2026 - 19:40:34 Line coverage: 58.4% (886/1516) Branch coverage: 52.9% (338/638) Total lines: 3791 Tag: Kestrun/Kestrun@eeafbe813231ed23417e7b339e170e307b2c86f9

Coverage delta

Coverage delta 4 -4

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()50%44100%
File 6: get_Builder()100%11100%
File 6: get_App()100%22100%
File 6: get_Runtime()100%11100%
File 6: get_ApplicationName()100%22100%
File 6: get_Options()100%11100%
File 6: .ctor(...)75%4484.84%
File 6: get_IsConfigured()100%11100%
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_PowerShellErrorResponseScript()100%11100%
File 6: set_PowerShellErrorResponseScript(...)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: AddFormOption(...)50%7670%
File 6: GetFormOption(...)50%22100%
File 6: AddFormPartRule(...)100%66100%
File 6: GetFormPartRule(...)0%620%
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(...)92.85%141495.23%
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%101090%
File 6: NormalizeHttp3ListenersAndConfigureQuic()16.66%21625%
File 6: IsQuicSupported()100%11100%
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%1173.07%
File 6: PopulateAppUrlsFromListeners()90%202090.9%
File 6: ResolveEphemeralPort(...)75%4490%
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()87.5%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    {
 1106253        foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()
 735254            .Where(a => !a.IsDynamic && IsManaged(a.Location)))
 255        {
 551256            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    {
 1946373        try { _ = AssemblyName.GetAssemblyName(path); return true; }
 344374        catch { return false; }          // native ⇒ BadImageFormatException
 1059375    }
 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;
 32using Kestrun.Forms;
 33
 34namespace Kestrun.Hosting;
 35
 36/// <summary>
 37/// Provides hosting and configuration for the Kestrun application, including service registration, middleware setup, an
 38/// </summary>
 39public partial class KestrunHost : IDisposable
 40{
 41    private const string KestrunVariableMarkerKey = "__kestrunVariable";
 42
 43    #region Static Members
 44    private static readonly JsonSerializerOptions JsonOptions;
 45
 46    static KestrunHost()
 47    {
 148        JsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
 149        {
 150            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
 151            WriteIndented = false
 152        };
 153        JsonOptions.Converters.Add(new JsonStringEnumConverter());
 154    }
 55    #endregion
 56
 57    #region Fields
 297058    internal WebApplicationBuilder Builder { get; }
 59
 60    private WebApplication? _app;
 61
 13162    internal WebApplication App => _app ?? throw new InvalidOperationException("WebApplication is not built yet. Call Bu
 63
 64    /// <summary>
 65    /// Gets the runtime information for the Kestrun host.
 66    /// </summary>
 72567    public KestrunHostRuntime Runtime { get; } = new();
 68
 69    /// <summary>
 70    /// Gets the application name for the Kestrun host.
 71    /// </summary>
 272    public string ApplicationName => Options.ApplicationName ?? "KestrunApp";
 73
 74    /// <summary>
 75    /// Gets the configuration options for the Kestrun host.
 76    /// </summary>
 201977    public KestrunOptions Options { get; private set; } = new();
 78
 79    /// <summary>
 80    /// List of PowerShell module paths to be loaded.
 81    /// </summary>
 63682    private readonly List<string> _modulePaths = [];
 83
 84    /// <summary>
 85    /// Indicates whether the Kestrun host is stopping.
 86    /// </summary>
 87    private int _stopping; // 0 = running, 1 = stopping
 88
 89    /// <summary>
 90    /// Indicates whether the Kestrun host configuration has been applied.
 91    /// </summary>
 50792    public bool IsConfigured { get; private set; }
 93
 94    /// <summary>
 95    /// The runspace pool manager for PowerShell execution.
 96    /// </summary>
 97    private KestrunRunspacePoolManager? _runspacePool;
 98
 99    /// <summary>
 100    /// Status code options for configuring status code pages.
 101    /// </summary>
 102    private StatusCodeOptions? _statusCodeOptions;
 103    /// <summary>
 104    /// Exception options for configuring exception handling.
 105    /// </summary>
 106    private ExceptionOptions? _exceptionOptions;
 107    private string? _powerShellErrorResponseScript;
 108    /// <summary>
 109    /// Forwarded headers options for configuring forwarded headers handling.
 110    /// </summary>
 111    private ForwardedHeadersOptions? _forwardedHeaderOptions;
 112
 8113    internal KestrunRunspacePoolManager RunspacePool => _runspacePool ?? throw new InvalidOperationException("Runspace p
 114
 115    // ── ✦ QUEUE #1 : SERVICE REGISTRATION ✦ ─────────────────────────────
 636116    private readonly List<Action<IServiceCollection>> _serviceQueue = [];
 117
 118    // ── ✦ QUEUE #2 : MIDDLEWARE STAGES ✦ ────────────────────────────────
 636119    private readonly List<Action<IApplicationBuilder>> _middlewareQueue = [];
 120
 749121    internal List<Action<KestrunHost>> FeatureQueue { get; } = [];
 122
 816123    internal List<IProbe> HealthProbes { get; } = [];
 124#if NET9_0_OR_GREATER
 125    private readonly Lock _healthProbeLock = new();
 126#else
 636127    private readonly object _healthProbeLock = new();
 128#endif
 129
 636130    internal readonly Dictionary<(string Pattern, HttpVerb Method), MapRouteOptions> _registeredRoutes =
 636131#pragma warning disable IDE0028 // Simplify collection initialization
 636132    new(new RouteKeyComparer());
 133#pragma warning restore IDE0028 // Simplify collection initialization
 134
 135    //internal readonly Dictionary<(string Scheme, string Type), AuthenticationSchemeOptions> _registeredAuthentications
 136    //  new(new AuthKeyComparer());
 137
 138    /// <summary>
 139    /// Gets the root directory path for the Kestrun application.
 140    /// </summary>
 149141    public string? KestrunRoot { get; private set; }
 142
 143    /// <summary>
 144    /// Gets the collection of module paths to be loaded by the Kestrun host.
 145    /// </summary>
 0146    public List<string> ModulePaths => _modulePaths;
 147
 148    /// <summary>
 149    /// Gets the shared state store for managing shared data across requests and sessions.
 150    /// </summary>
 206151    public SharedState.SharedState SharedState { get; }
 152
 153    /// <summary>
 154    /// Gets the Serilog logger instance used by the Kestrun host.
 155    /// </summary>
 12182156    public Serilog.ILogger Logger { get; private set; }
 157
 158    private SchedulerService? _scheduler;
 159    /// <summary>
 160    /// Gets the scheduler service used for managing scheduled tasks in the Kestrun host.
 161    /// Initialized in ConfigureServices via AddScheduler()
 162    /// </summary>
 163    public SchedulerService Scheduler
 164    {
 1165        get => _scheduler ?? throw new InvalidOperationException("SchedulerService is not initialized. Call AddScheduler
 1166        internal set => _scheduler = value;
 167    }
 168
 169    private KestrunTaskService? _tasks;
 170    /// <summary>
 171    /// Gets the ad-hoc task service used for running one-off tasks (PowerShell, C#, VB.NET).
 172    /// Initialized via AddTasks()
 173    /// </summary>
 174    public KestrunTaskService Tasks
 175    {
 0176        get => _tasks ?? throw new InvalidOperationException("Tasks is not initialized. Call AddTasks() to enable task m
 0177        internal set => _tasks = value;
 178    }
 179
 180    /// <summary>
 181    /// Gets the stack used for managing route groups in the Kestrun host.
 182    /// </summary>
 636183    public System.Collections.Stack RouteGroupStack { get; } = new();
 184
 185    /// <summary>
 186    /// Gets the registered routes in the Kestrun host.
 187    /// </summary>
 179188    public Dictionary<(string, HttpVerb), MapRouteOptions> RegisteredRoutes => _registeredRoutes;
 189
 190    /// <summary>
 191    /// Gets the registered authentication schemes in the Kestrun host.
 192    /// </summary>
 659193    public AuthenticationRegistry RegisteredAuthentications { get; } = new();
 194
 195    /// <summary>
 196    /// Gets or sets the default cache control settings for HTTP responses.
 197    /// </summary>
 9198    public CacheControlHeaderValue? DefaultCacheControl { get; internal set; }
 199
 200    /// <summary>
 201    /// Gets the shared state manager for managing shared data across requests and sessions.
 202    /// </summary>
 130203    public bool PowershellMiddlewareEnabled { get; set; } = false;
 204
 205    /// <summary>
 206    /// The localization store used by this host when `UseKestrunLocalization` is configured.
 207    /// May be null if localization middleware was not added.
 208    /// </summary>
 0209    public KestrunLocalizationStore? LocalizationStore { get; internal set; }
 210
 211    /// <summary>
 212    /// Gets or sets a value indicating whether this instance is the default Kestrun host.
 213    /// </summary>
 1214    public bool DefaultHost { get; internal set; }
 215
 216    /// <summary>
 217    /// The list of CORS policy names that have been defined in the KestrunHost instance.
 218    /// </summary>
 820219    public List<string> DefinedCorsPolicyNames { get; } = [];
 220
 221    /// <summary>
 222    /// Gets or sets a value indicating whether CORS (Cross-Origin Resource Sharing) is enabled.
 223    /// </summary>
 156224    public bool CorsPolicyDefined => DefinedCorsPolicyNames.Count > 0;
 225
 226    /// <summary>
 227    /// Gets the scanned OpenAPI component annotations from PowerShell scripts.
 228    /// </summary>
 63229    public Dictionary<string, OpenApiComponentAnnotationScanner.AnnotatedVariable>? ComponentAnnotations { get; private 
 230
 231    /// <summary>
 232    /// Gets or sets the status code options for configuring status code pages.
 233    /// </summary>
 234    public StatusCodeOptions? StatusCodeOptions
 235    {
 110236        get => _statusCodeOptions;
 237        set
 238        {
 0239            if (IsConfigured)
 240            {
 0241                throw new InvalidOperationException("Cannot modify StatusCodeOptions after configuration is applied.");
 242            }
 0243            _statusCodeOptions = value;
 0244        }
 245    }
 246
 247    /// <summary>
 248    /// Gets or sets the exception options for configuring exception handling.
 249    /// </summary>
 250    public ExceptionOptions? ExceptionOptions
 251    {
 123252        get => _exceptionOptions;
 253        set
 254        {
 5255            if (IsConfigured)
 256            {
 0257                throw new InvalidOperationException("Cannot modify ExceptionOptions after configuration is applied.");
 258            }
 5259            _exceptionOptions = value;
 5260        }
 261    }
 262
 263    /// <summary>
 264    /// Gets or sets an optional PowerShell script used by PowerShell route execution to generate error responses.
 265    /// The script executes in the request runspace and is responsible for writing the response.
 266    /// </summary>
 267    public string? PowerShellErrorResponseScript
 268    {
 2269        get => _powerShellErrorResponseScript;
 270        set
 271        {
 2272            if (IsConfigured)
 273            {
 0274                throw new InvalidOperationException("Cannot modify PowerShellErrorResponseScript after configuration is 
 275            }
 276
 2277            _powerShellErrorResponseScript = value;
 2278        }
 279    }
 280
 281    /// <summary>
 282    /// Gets or sets the forwarded headers options for configuring forwarded headers handling.
 283    /// </summary>
 284    public ForwardedHeadersOptions? ForwardedHeaderOptions
 285    {
 113286        get => _forwardedHeaderOptions;
 287        set
 288        {
 4289            if (IsConfigured)
 290            {
 1291                throw new InvalidOperationException("Cannot modify ForwardedHeaderOptions after configuration is applied
 292            }
 3293            _forwardedHeaderOptions = value;
 3294        }
 295    }
 296
 297    /// <summary>
 298    /// Gets the antiforgery options for configuring antiforgery token generation and validation.
 299    /// </summary>
 0300    public AntiforgeryOptions? AntiforgeryOptions { get; set; }
 301
 302    /// <summary>
 303    /// Gets the OpenAPI document descriptor for configuring OpenAPI generation.
 304    /// </summary>
 747305    public Dictionary<string, OpenApiDocDescriptor> OpenApiDocumentDescriptor { get; } = [];
 306
 307    /// <summary>
 308    /// Gets the IDs of all OpenAPI documents configured in the Kestrun host.
 309    /// </summary>
 0310    public string[] OpenApiDocumentIds => [.. OpenApiDocumentDescriptor.Keys];
 311
 312    /// <summary>
 313    /// Gets the default OpenAPI document descriptor.
 314    /// </summary>
 315    public OpenApiDocDescriptor? DefaultOpenApiDocumentDescriptor
 0316        => OpenApiDocumentDescriptor.FirstOrDefault().Value;
 317
 318    #endregion
 319
 320    // Accepts optional module paths (from PowerShell)
 321    #region Constructor
 322
 323    /// <summary>
 324    /// Initializes a new instance of the <see cref="KestrunHost"/> class with the specified application name, root dire
 325    /// </summary>
 326    /// <param name="appName">The name of the application.</param>
 327    /// <param name="kestrunRoot">The root directory for the Kestrun application.</param>
 328    /// <param name="modulePathsObj">An array of module paths to be loaded.</param>
 329    public KestrunHost(string? appName, string? kestrunRoot = null, string[]? modulePathsObj = null) :
 108330            this(appName, Log.Logger, kestrunRoot, modulePathsObj)
 108331    { }
 332
 333    /// <summary>
 334    /// Initializes a new instance of the <see cref="KestrunHost"/> class with the specified application name and logger
 335    /// </summary>
 336    /// <param name="appName">The name of the application.</param>
 337    /// <param name="logger">The Serilog logger instance to use.</param>
 338    /// <param name="ordinalIgnoreCase">Indicates whether the shared state should be case-insensitive.</param>
 339    public KestrunHost(string? appName, Serilog.ILogger logger,
 0340          bool ordinalIgnoreCase) : this(appName, logger, null, null, null, ordinalIgnoreCase)
 0341    { }
 342
 343    /// <summary>
 344    /// Initializes a new instance of the <see cref="KestrunHost"/> class with the specified application name, logger, r
 345    /// </summary>
 346    /// <param name="appName">The name of the application.</param>
 347    /// <param name="logger">The Serilog logger instance to use.</param>
 348    /// <param name="kestrunRoot">The root directory for the Kestrun application.</param>
 349    /// <param name="modulePathsObj">An array of module paths to be loaded.</param>
 350    /// <param name="args">Command line arguments to pass to the application.</param>
 351    /// <param name="ordinalIgnoreCase">Indicates whether the shared state should be case-insensitive.</param>
 636352    public KestrunHost(string? appName, Serilog.ILogger logger,
 636353    string? kestrunRoot = null, string[]? modulePathsObj = null, string[]? args = null, bool ordinalIgnoreCase = true)
 354    {
 355        // ① Logger
 636356        Logger = logger ?? Log.Logger;
 636357        LogConstructorArgs(appName, logger == null, kestrunRoot, modulePathsObj?.Length ?? 0);
 636358        SharedState = new(ordinalIgnoreCase: ordinalIgnoreCase);
 359        // ② Working directory/root
 636360        SetWorkingDirectoryIfNeeded(kestrunRoot);
 361
 362        // ③ Ensure Kestrun module path is available
 636363        AddKestrunModulePathIfMissing(modulePathsObj);
 364
 365        // ④ WebApplicationBuilder
 366        // NOTE:
 367        // ASP.NET Core's WebApplicationBuilder validates that ContentRootPath exists.
 368        // On Unix/macOS, the process current working directory (CWD) can be deleted by tests or external code.
 369        // If we derive ContentRootPath from a missing/deleted directory, CreateBuilder throws.
 370        // We therefore (a) choose an existing directory when possible and (b) retry with a stable fallback
 371        // to keep host creation resilient in CI where test ordering/parallelism can surface this.
 372        WebApplicationOptions CreateWebAppOptions(string contentRootPath)
 373        {
 636374            return new()
 636375            {
 636376                ContentRootPath = contentRootPath,
 636377                Args = args ?? [],
 636378                EnvironmentName = EnvironmentHelper.Name
 636379            };
 380        }
 381
 636382        var contentRootPath = GetSafeContentRootPath(kestrunRoot);
 383
 384        try
 385        {
 636386            Builder = WebApplication.CreateBuilder(CreateWebAppOptions(contentRootPath));
 636387        }
 0388        catch (ArgumentException ex) when (
 0389            string.Equals(ex.ParamName, "contentRootPath", StringComparison.OrdinalIgnoreCase) &&
 0390            !string.Equals(contentRootPath, AppContext.BaseDirectory, StringComparison.Ordinal))
 391        {
 392            // The selected content root may have been deleted between resolution and builder initialization
 393            // (TOCTOU race) or the process CWD may have become invalid. Fall back to a stable path so host
 394            // creation does not fail.
 0395            Builder = WebApplication.CreateBuilder(CreateWebAppOptions(AppContext.BaseDirectory));
 0396        }
 397        // ✅ add here, after Builder is definitely assigned
 636398        _ = Builder.Services.Configure<HostOptions>(o =>
 636399        {
 110400            _ = o.ShutdownTimeout = TimeSpan.FromSeconds(5);
 746401        });
 402
 403        // Enable Serilog for the host
 636404        _ = Builder.Host.UseSerilog();
 405
 406        // Make this KestrunHost available via DI so framework-created components (e.g., auth handlers)
 407        // can resolve it. We register the current instance as a singleton.
 636408        _ = Builder.Services.AddSingleton(this);
 409
 410        // Expose Serilog.ILogger via DI for components (e.g., SignalR hubs) that depend on Serilog's logger
 411        // ASP.NET Core registers Microsoft.Extensions.Logging.ILogger by default; we also bind Serilog.ILogger
 412        // to the same instance so constructors like `KestrunHub(Serilog.ILogger logger)` resolve properly.
 636413        _ = Builder.Services.AddSingleton(Logger);
 414
 415        // ⑤ Options
 636416        InitializeOptions(appName);
 417
 418        // ⑥ Add user-provided module paths
 636419        AddUserModulePaths(modulePathsObj);
 420
 636421        Logger.Information("Current working directory: {CurrentDirectory}", GetSafeCurrentDirectory());
 636422    }
 423    #endregion
 424
 425    #region Helpers
 426
 427    /// <summary>
 428    /// Adds a form parsing option for the specified name.
 429    /// </summary>
 430    /// <param name="options">The form options to add.</param>
 431    /// <returns>True if the option was added successfully; otherwise, false.</returns>
 432    public bool AddFormOption(KrFormOptions options)
 433    {
 5434        ArgumentNullException.ThrowIfNull(options);
 5435        ArgumentNullException.ThrowIfNull(options.Name);
 436
 5437        if (Runtime.FormOptions.TryAdd(options.Name, options))
 438        {
 439            // Link scoped rules under their container rule(s) once at configuration-time.
 440            // This keeps KrFormPartRule.NestedRules useful for introspection/debugging.
 5441            FormHelper.PopulateNestedRulesFromScopes(options);
 442
 5443            if (Logger.IsEnabled(LogEventLevel.Debug))
 444            {
 5445                Logger.Debug("Added form option with name '{FormOptionName}'.", options.Name);
 446            }
 5447            return true;
 448        }
 449        else
 450        {
 0451            if (Logger.IsEnabled(LogEventLevel.Warning))
 452            {
 0453                Logger.Warning("Form option with name '{FormOptionName}' already exists. Skipping addition.", options.Na
 454            }
 0455            return false;
 456        }
 457    }
 458
 459    /// <summary>
 460    /// Gets the form parsing option for the specified name.
 461    /// </summary>
 462    /// <param name="name">The name of the form option.</param>
 463    /// <returns>The form options if found; otherwise, null.</returns>
 1464    public KrFormOptions? GetFormOption(string name) => Runtime.FormOptions.TryGetValue(name, out var options) ? options
 465
 466    /// <summary>
 467    /// Adds a form part rule for the specified name.
 468    /// </summary>
 469    /// <param name="ruleOptions">The form part rule to add.</param>
 470    /// <returns>True if the rule was added successfully; otherwise, false.</returns>
 471    public bool AddFormPartRule(KrFormPartRule ruleOptions)
 472    {
 34473        ArgumentNullException.ThrowIfNull(ruleOptions);
 34474        ArgumentNullException.ThrowIfNull(ruleOptions.Name);
 475
 34476        if (Runtime.FormPartRules.TryAdd(ruleOptions.Name, ruleOptions))
 477        {
 22478            if (Logger.IsEnabled(LogEventLevel.Debug))
 479            {
 22480                Logger.Debug("Added form part rule with name '{FormPartRuleName}'.", ruleOptions.Name);
 481            }
 22482            return true;
 483        }
 484        else
 485        {
 12486            if (Logger.IsEnabled(LogEventLevel.Warning))
 487            {
 12488                Logger.Warning("Form part rule with name '{FormPartRuleName}' already exists. Skipping addition.", ruleO
 489            }
 12490            return false;
 491        }
 492    }
 493
 494    /// <summary>
 495    /// Gets the form part rule for the specified name.
 496    /// </summary>
 497    /// <param name="name">The name of the form part rule.</param>
 498    /// <returns>The form part rule if found; otherwise, null.</returns>
 0499    public KrFormPartRule? GetFormPartRule(string name) => Runtime.FormPartRules.TryGetValue(name, out var options) ? op
 500
 501    /// <summary>
 502    /// Gets the OpenAPI document descriptor for the specified document ID.
 503    /// </summary>
 504    /// <param name="apiDocId">The ID of the OpenAPI document.</param>
 505    /// <returns>The OpenAPI document descriptor.</returns>
 506    public OpenApiDocDescriptor GetOrCreateOpenApiDocument(string apiDocId)
 507    {
 28508        if (string.IsNullOrWhiteSpace(apiDocId))
 509        {
 0510            throw new ArgumentException("Document ID cannot be null or whitespace.", nameof(apiDocId));
 511        }
 512        // Check if descriptor already exists
 28513        if (OpenApiDocumentDescriptor.TryGetValue(apiDocId, out var descriptor))
 514        {
 5515            if (Logger.IsEnabled(LogEventLevel.Debug))
 516            {
 5517                Logger.Debug("OpenAPI document descriptor for ID '{DocId}' already exists. Returning existing descriptor
 518            }
 519        }
 520        else
 521        {
 23522            descriptor = new OpenApiDocDescriptor(this, apiDocId);
 23523            OpenApiDocumentDescriptor[apiDocId] = descriptor;
 524        }
 28525        return descriptor;
 526    }
 527
 528    /// <summary>
 529    /// Gets the list of OpenAPI document descriptors for the specified document IDs.
 530    /// </summary>
 531    /// <param name="openApiDocIds"> The array of OpenAPI document IDs.</param>
 532    /// <returns>A list of OpenApiDocDescriptor objects corresponding to the provided document IDs.</returns>
 533    public List<OpenApiDocDescriptor> GetOrCreateOpenApiDocument(string[] openApiDocIds)
 534    {
 2535        var list = new List<OpenApiDocDescriptor>();
 8536        foreach (var apiDocId in openApiDocIds)
 537        {
 2538            list.Add(GetOrCreateOpenApiDocument(apiDocId));
 539        }
 2540        return list;
 541    }
 542
 543    /// <summary>
 544    /// Logs constructor arguments at Debug level for diagnostics.
 545    /// </summary>
 546    private void LogConstructorArgs(string? appName, bool defaultLogger, string? kestrunRoot, int modulePathsLength)
 547    {
 636548        if (Logger.IsEnabled(LogEventLevel.Debug))
 549        {
 464550            Logger.Debug(
 464551                "KestrunHost ctor: AppName={AppName}, DefaultLogger={DefaultLogger}, KestrunRoot={KestrunRoot}, ModulePa
 464552                appName, defaultLogger, kestrunRoot, modulePathsLength);
 553        }
 636554    }
 555
 556    /// <summary>
 557    /// Sets the current working directory to the provided Kestrun root if needed and stores it.
 558    /// </summary>
 559    /// <param name="kestrunRoot">The Kestrun root directory path.</param>
 560    private void SetWorkingDirectoryIfNeeded(string? kestrunRoot)
 561    {
 636562        if (string.IsNullOrWhiteSpace(kestrunRoot))
 563        {
 488564            return;
 565        }
 566
 148567        if (!string.Equals(GetSafeCurrentDirectory(), kestrunRoot, StringComparison.Ordinal))
 568        {
 111569            Directory.SetCurrentDirectory(kestrunRoot);
 111570            Logger.Information("Changed current directory to Kestrun root: {KestrunRoot}", kestrunRoot);
 571        }
 572        else
 573        {
 37574            Logger.Verbose("Current directory is already set to Kestrun root: {KestrunRoot}", kestrunRoot);
 575        }
 576
 148577        KestrunRoot = kestrunRoot;
 148578    }
 579
 580    private static string GetSafeContentRootPath(string? kestrunRoot)
 581    {
 636582        var candidate = !string.IsNullOrWhiteSpace(kestrunRoot)
 636583            ? kestrunRoot
 636584            : GetSafeCurrentDirectory();
 585
 586        // WebApplication.CreateBuilder requires that ContentRootPath exists.
 587        // On Unix/macOS, getcwd() can fail (or return a path that was deleted) if the CWD was removed.
 588        // This can happen in tests that use temp directories and delete them after constructing a host.
 589        // Guard here to avoid injecting a non-existent content root into ASP.NET Core.
 636590        return Directory.Exists(candidate)
 636591            ? candidate
 636592            : AppContext.BaseDirectory;
 593    }
 594
 595    private static string GetSafeCurrentDirectory()
 596    {
 597        try
 598        {
 1382599            return Directory.GetCurrentDirectory();
 600        }
 2601        catch (Exception ex) when (
 2602            ex is IOException or
 2603            UnauthorizedAccessException or
 2604            DirectoryNotFoundException or
 2605            FileNotFoundException)
 606        {
 607            // On Unix/macOS, getcwd() can fail with ENOENT if the CWD was deleted.
 608            // Fall back to the app base directory to keep host creation resilient.
 2609            return AppContext.BaseDirectory;
 610        }
 1382611    }
 612
 613    /// <summary>
 614    /// Ensures the core Kestrun module path is present; if missing, locates and adds it.
 615    /// </summary>
 616    /// <param name="modulePathsObj">The array of module paths to check.</param>
 617    private void AddKestrunModulePathIfMissing(string[]? modulePathsObj)
 618    {
 636619        var needsLocate = modulePathsObj is null ||
 673620                          (modulePathsObj?.Any(p => p.Contains("Kestrun.psm1", StringComparison.Ordinal)) == false);
 636621        if (!needsLocate)
 622        {
 37623            return;
 624        }
 625
 599626        var kestrunModulePath = PowerShellModuleLocator.LocateKestrunModule();
 599627        if (string.IsNullOrWhiteSpace(kestrunModulePath))
 628        {
 0629            Logger.Fatal("Kestrun module not found. Ensure the Kestrun module is installed.");
 0630            throw new FileNotFoundException("Kestrun module not found.");
 631        }
 632
 599633        Logger.Information("Found Kestrun module at: {KestrunModulePath}", kestrunModulePath);
 599634        Logger.Verbose("Adding Kestrun module path: {KestrunModulePath}", kestrunModulePath);
 599635        _modulePaths.Add(kestrunModulePath);
 599636    }
 637
 638    /// <summary>
 639    /// Initializes Kestrun options and sets the application name when provided.
 640    /// </summary>
 641    /// <param name="appName">The name of the application.</param>
 642    private void InitializeOptions(string? appName)
 643    {
 636644        if (string.IsNullOrEmpty(appName))
 645        {
 1646            Logger.Information("No application name provided, using default.");
 1647            Options = new KestrunOptions();
 648        }
 649        else
 650        {
 635651            Logger.Information("Setting application name: {AppName}", appName);
 635652            Options = new KestrunOptions { ApplicationName = appName };
 653        }
 635654    }
 655
 656    /// <summary>
 657    /// Adds user-provided module paths if they exist, logging warnings for invalid entries.
 658    /// </summary>
 659    /// <param name="modulePathsObj">The array of module paths to check.</param>
 660    private void AddUserModulePaths(string[]? modulePathsObj)
 661    {
 636662        if (modulePathsObj is IEnumerable<object> modulePathsEnum)
 663        {
 148664            foreach (var modPathObj in modulePathsEnum)
 665            {
 37666                if (modPathObj is string modPath && !string.IsNullOrWhiteSpace(modPath))
 667                {
 37668                    if (File.Exists(modPath))
 669                    {
 37670                        Logger.Information("[KestrunHost] Adding module path: {ModPath}", modPath);
 37671                        _modulePaths.Add(modPath);
 672                    }
 673                    else
 674                    {
 0675                        Logger.Warning("[KestrunHost] Module path does not exist: {ModPath}", modPath);
 676                    }
 677                }
 678                else
 679                {
 0680                    Logger.Warning("[KestrunHost] Invalid module path provided.");
 681                }
 682            }
 683        }
 636684    }
 685    #endregion
 686
 687    #region Health Probes
 688
 689    /// <summary>
 690    /// Registers the provided <see cref="IProbe"/> instance with the host.
 691    /// </summary>
 692    /// <param name="probe">The probe to register.</param>
 693    /// <returns>The current <see cref="KestrunHost"/> instance.</returns>
 694    public KestrunHost AddProbe(IProbe probe)
 695    {
 0696        ArgumentNullException.ThrowIfNull(probe);
 0697        RegisterProbeInternal(probe);
 0698        return this;
 699    }
 700
 701    /// <summary>
 702    /// Registers a delegate-based probe.
 703    /// </summary>
 704    /// <param name="name">Probe name.</param>
 705    /// <param name="tags">Optional tag list used for filtering.</param>
 706    /// <param name="callback">Delegate executed when the probe runs.</param>
 707    /// <returns>The current <see cref="KestrunHost"/> instance.</returns>
 708    public KestrunHost AddProbe(string name, string[]? tags, Func<CancellationToken, Task<ProbeResult>> callback)
 709    {
 0710        ArgumentException.ThrowIfNullOrEmpty(name);
 0711        ArgumentNullException.ThrowIfNull(callback);
 712
 0713        var probe = new DelegateProbe(name, tags, callback);
 0714        RegisterProbeInternal(probe);
 0715        return this;
 716    }
 717
 718    /// <summary>
 719    /// Registers a script-based probe written in any supported language.
 720    /// </summary>
 721    /// <param name="name">Probe name.</param>
 722    /// <param name="tags">Optional tag list used for filtering.</param>
 723    /// <param name="code">Script contents.</param>
 724    /// <param name="language">Optional language override. When null, <see cref="KestrunOptions.Health"/> defaults are u
 725    /// <param name="arguments">Optional argument dictionary exposed to the script.</param>
 726    /// <param name="extraImports">Optional language-specific imports.</param>
 727    /// <param name="extraRefs">Optional additional assembly references.</param>
 728    /// <returns>The current <see cref="KestrunHost"/> instance.</returns>
 729    public KestrunHost AddProbe(
 730        string name,
 731        string[]? tags,
 732        string code,
 733        ScriptLanguage? language = null,
 734        IReadOnlyDictionary<string, object?>? arguments = null,
 735        string[]? extraImports = null,
 736        Assembly[]? extraRefs = null)
 737    {
 0738        ArgumentException.ThrowIfNullOrEmpty(name);
 0739        ArgumentException.ThrowIfNullOrEmpty(code);
 740
 0741        var effectiveLanguage = language ?? Options.Health.DefaultScriptLanguage;
 0742        var logger = Logger.ForContext("HealthProbe", name);
 0743        var probe = ScriptProbeFactory.Create(host: this, name: name, tags: tags,
 0744            effectiveLanguage, code: code,
 0745            runspaceAccessor: effectiveLanguage == ScriptLanguage.PowerShell ? () => RunspacePool : null,
 0746            arguments: arguments, extraImports: extraImports, extraRefs: extraRefs);
 747
 0748        RegisterProbeInternal(probe);
 0749        return this;
 750    }
 751
 752    /// <summary>
 753    /// Returns a snapshot of the currently registered probes.
 754    /// </summary>
 755    internal IReadOnlyList<IProbe> GetHealthProbesSnapshot()
 756    {
 0757        lock (_healthProbeLock)
 758        {
 0759            return [.. HealthProbes];
 760        }
 0761    }
 762
 763    private void RegisterProbeInternal(IProbe probe)
 764    {
 60765        lock (_healthProbeLock)
 766        {
 60767            var index = HealthProbes.FindIndex(p => string.Equals(p.Name, probe.Name, StringComparison.OrdinalIgnoreCase
 60768            if (index >= 0)
 769            {
 0770                HealthProbes[index] = probe;
 0771                Logger.Information("Replaced health probe {ProbeName}.", probe.Name);
 772            }
 773            else
 774            {
 60775                HealthProbes.Add(probe);
 60776                Logger.Information("Registered health probe {ProbeName}.", probe.Name);
 777            }
 60778        }
 60779    }
 780
 781    #endregion
 782    #region OpenAPI
 783
 784    /// <summary>
 785    /// Adds callback automation middleware to the Kestrun host.
 786    /// </summary>
 787    /// <param name="options">Optional callback dispatch options.</param>
 788    /// <returns>The updated Kestrun host.</returns>
 789    public KestrunHost AddCallbacksAutomation(CallbackDispatchOptions? options = null)
 790    {
 0791        if (Logger.IsEnabled(LogEventLevel.Debug))
 792        {
 0793            Logger.Debug(
 0794                "Adding callback automation middleware (custom configuration supplied: {HasConfig})",
 0795                options != null);
 796        }
 0797        options ??= new CallbackDispatchOptions();
 0798        if (Logger.IsEnabled(LogEventLevel.Debug))
 799        {
 0800            Logger.Debug("Adding callback automation middleware with options: {@Options}", options);
 801        }
 802
 0803        _ = AddService(services =>
 0804        {
 0805            _ = services.AddSingleton(options ?? new CallbackDispatchOptions());
 0806            _ = services.AddSingleton<InMemoryCallbackQueue>();
 0807            _ = services.AddSingleton<ICallbackDispatcher, InMemoryCallbackDispatcher>();
 0808            _ = services.AddHostedService<InMemoryCallbackDispatchWorker>();
 0809            _ = services.AddHttpClient("kestrun-callbacks", c =>
 0810            {
 0811                c.Timeout = options?.DefaultTimeout ?? TimeSpan.FromSeconds(30);
 0812            });
 0813            _ = services.AddSingleton<ICallbackRetryPolicy>(sp =>
 0814                {
 0815                    return new DefaultCallbackRetryPolicy(options);
 0816                });
 0817
 0818            _ = services.AddSingleton<ICallbackUrlResolver, DefaultCallbackUrlResolver>();
 0819            _ = services.AddSingleton<ICallbackBodySerializer, JsonCallbackBodySerializer>();
 0820
 0821            _ = services.AddHttpClient<ICallbackSender, HttpCallbackSender>();
 0822
 0823            _ = services.AddHostedService<CallbackWorker>();
 0824        });
 0825        return this;
 826    }
 827    #endregion
 828    #region ListenerOptions
 829
 830    /// <summary>
 831    /// Configures a listener for the Kestrun host with the specified port, optional IP address, certificate, protocols,
 832    /// </summary>
 833    /// <param name="port">The port number to listen on.</param>
 834    /// <param name="ipAddress">The IP address to bind to. If null, binds to any address.</param>
 835    /// <param name="x509Certificate">The X509 certificate for HTTPS. If null, HTTPS is not used.</param>
 836    /// <param name="protocols">The HTTP protocols to use.</param>
 837    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 838    /// <returns>The current KestrunHost instance.</returns>
 839    public KestrunHost ConfigureListener(
 840    int port,
 841    IPAddress? ipAddress = null,
 842    X509Certificate2? x509Certificate = null,
 843    HttpProtocols protocols = HttpProtocols.Http1,
 844    bool useConnectionLogging = false)
 845    {
 41846        if (Logger.IsEnabled(LogEventLevel.Debug))
 847        {
 18848            Logger.Debug("ConfigureListener port={Port}, ipAddress={IPAddress}, protocols={Protocols}, useConnectionLogg
 849        }
 850        // Validate state
 41851        if (IsConfigured)
 852        {
 0853            throw new InvalidOperationException("Cannot configure listeners after configuration is applied.");
 854        }
 855        // Validate protocols
 41856        if (protocols == HttpProtocols.Http1AndHttp2AndHttp3 && !IsQuicSupported())
 857        {
 4858            Logger.Warning("HTTP/3 cannot be enabled because QUIC/libmsquic is not available on this platform. Falling b
 859
 4860            protocols = HttpProtocols.Http1AndHttp2;
 861        }
 862        // Resolve dynamic port when requested
 41863        if (port == 0)
 864        {
 23865            var bindAddress = ipAddress ?? IPAddress.Any;
 23866            port = ResolveEphemeralPort(bindAddress);
 23867            Logger.Information("Selected ephemeral port {Port} for listener on {Address}", port, bindAddress);
 868        }
 869        // Add listener
 41870        Options.Listeners.Add(new ListenerOptions
 41871        {
 41872            IPAddress = ipAddress ?? IPAddress.Any,
 41873            Port = port,
 41874            UseHttps = x509Certificate != null,
 41875            X509Certificate = x509Certificate,
 41876            Protocols = protocols,
 41877            UseConnectionLogging = useConnectionLogging
 41878        });
 41879        return this;
 880    }
 881
 882    /// <summary>
 883    /// Configures a listener for the Kestrun host with the specified port, optional IP address, and connection logging.
 884    /// </summary>
 885    /// <param name="port">The port number to listen on.</param>
 886    /// <param name="ipAddress">The IP address to bind to. If null, binds to any address.</param>
 887    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 888    public void ConfigureListener(
 889    int port,
 890    IPAddress? ipAddress = null,
 24891    bool useConnectionLogging = false) => _ = ConfigureListener(port: port, ipAddress: ipAddress, x509Certificate: null,
 892
 893    /// <summary>
 894    /// Configures a listener for the Kestrun host with the specified port and connection logging option.
 895    /// </summary>
 896    /// <param name="port">The port number to listen on.</param>
 897    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 898    public void ConfigureListener(
 899    int port,
 1900    bool useConnectionLogging = false) => _ = ConfigureListener(port: port, ipAddress: null, x509Certificate: null, prot
 901
 902    /// <summary>
 903    /// Configures listeners for the Kestrun host by resolving the specified host name to IP addresses and binding to ea
 904    /// </summary>
 905    /// <param name="hostName">The host name to resolve and bind to.</param>
 906    /// <param name="port">The port number to listen on.</param>
 907    /// <param name="x509Certificate">The X509 certificate for HTTPS. If null, HTTPS is not used.</param>
 908    /// <param name="protocols">The HTTP protocols to use.</param>
 909    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 910    /// <param name="families">Optional array of address families to filter resolved addresses (e.g., IPv4-only).</param
 911    /// <returns>The current KestrunHost instance.</returns>
 912    /// <exception cref="ArgumentException">Thrown when the host name is null or whitespace.</exception>
 913    /// <exception cref="InvalidOperationException">Thrown when no valid IP addresses are resolved.</exception>
 914    public KestrunHost ConfigureListener(
 915    string hostName,
 916    int port,
 917    X509Certificate2? x509Certificate = null,
 918    HttpProtocols protocols = HttpProtocols.Http1,
 919    bool useConnectionLogging = false,
 920    AddressFamily[]? families = null) // e.g. new[] { AddressFamily.InterNetwork } for IPv4-only
 921    {
 0922        if (string.IsNullOrWhiteSpace(hostName))
 923        {
 0924            throw new ArgumentException("Host name must be provided.", nameof(hostName));
 925        }
 926
 927        // If caller passed an IP literal, just bind once.
 0928        if (IPAddress.TryParse(hostName, out var parsedIp))
 929        {
 0930            _ = ConfigureListener(port, parsedIp, x509Certificate, protocols, useConnectionLogging);
 0931            return this;
 932        }
 933
 934        // Resolve and bind to ALL matching addresses (IPv4/IPv6)
 0935        var addrs = Dns.GetHostAddresses(hostName)
 0936                       .Where(a => families is null || families.Length == 0 || families.Contains(a.AddressFamily))
 0937                       .Where(a => a.AddressFamily is AddressFamily.InterNetwork or AddressFamily.InterNetworkV6)
 0938                       .ToArray();
 939
 0940        if (addrs.Length == 0)
 941        {
 0942            throw new InvalidOperationException($"No IPv4/IPv6 addresses resolved for host '{hostName}'.");
 943        }
 944
 0945        foreach (var addr in addrs)
 946        {
 0947            _ = ConfigureListener(port, addr, x509Certificate, protocols, useConnectionLogging);
 948        }
 949
 0950        return this;
 951    }
 952
 953    /// <summary>
 954    /// Configures listeners for the Kestrun host based on the provided absolute URI, resolving the host to IP addresses
 955    /// </summary>
 956    /// <param name="uri">The absolute URI to configure the listener for.</param>
 957    /// <param name="x509Certificate">The X509 certificate for HTTPS. If null, HTTPS is not used.</param>
 958    /// <param name="protocols">The HTTP protocols to use.</param>
 959    /// <param name="useConnectionLogging">Specifies whether to enable connection logging.</param>
 960    /// <param name="families">Optional array of address families to filter resolved addresses (e.g., IPv4-only).</param
 961    /// <returns>The current KestrunHost instance.</returns>
 962    /// <exception cref="ArgumentException">Thrown when the provided URI is not absolute.</exception>
 963    /// <exception cref="InvalidOperationException">Thrown when no valid IP addresses are resolved.</exception>
 964    public KestrunHost ConfigureListener(
 965    Uri uri,
 966    X509Certificate2? x509Certificate = null,
 967    HttpProtocols? protocols = null,
 968    bool useConnectionLogging = false,
 969    AddressFamily[]? families = null)
 970    {
 0971        ArgumentNullException.ThrowIfNull(uri);
 972
 0973        if (!uri.IsAbsoluteUri)
 974        {
 0975            throw new ArgumentException("URL must be absolute.", nameof(uri));
 976        }
 977
 0978        var isHttps = uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase);
 0979        var port = uri.IsDefaultPort ? (isHttps ? 443 : 80) : uri.Port;
 980
 981        // Default: HTTPS → H1+H2, HTTP → H1
 0982        var chosenProtocols = protocols ?? (isHttps ? HttpProtocols.Http1AndHttp2 : HttpProtocols.Http1);
 983
 984        // Delegate to hostname overload (which will resolve or handle IP literal)
 0985        return ConfigureListener(
 0986            hostName: uri.Host,
 0987            port: port,
 0988            x509Certificate: x509Certificate,
 0989            protocols: chosenProtocols,
 0990            useConnectionLogging: useConnectionLogging,
 0991            families: families
 0992        );
 993    }
 994
 995    #endregion
 996
 997    #region Configuration
 998
 999    /// <summary>
 1000    /// Validates if configuration can be applied and returns early if already configured.
 1001    /// </summary>
 1002    /// <returns>True if configuration should proceed, false if it should be skipped.</returns>
 1003    internal bool ValidateConfiguration()
 1004    {
 841005        if (Logger.IsEnabled(LogEventLevel.Debug))
 1006        {
 411007            Logger.Debug("EnableConfiguration(options) called");
 1008        }
 1009
 841010        if (IsConfigured)
 1011        {
 221012            if (Logger.IsEnabled(LogEventLevel.Debug))
 1013            {
 21014                Logger.Debug("Configuration already applied, skipping");
 1015            }
 221016            return false; // Already configured
 1017        }
 1018
 621019        return true;
 1020    }
 1021
 1022    /// <summary>
 1023    /// Creates and initializes the runspace pool for PowerShell execution.
 1024    /// </summary>
 1025    /// <param name="userVariables">User-defined variables to inject into the runspace pool.</param>
 1026    /// <param name="userFunctions">User-defined functions to inject into the runspace pool.</param>
 1027    /// <param name="openApiClassesPath">Path to the OpenAPI class definitions to inject into the runspace pool.</param>
 1028    /// <exception cref="InvalidOperationException">Thrown when runspace pool creation fails.</exception>
 1029    internal void InitializeRunspacePool(Dictionary<string, object>? userVariables, Dictionary<string, string>? userFunc
 1030    {
 631031        _runspacePool =
 631032            CreateRunspacePool(Options.MaxRunspaces, userVariables, userFunctions, openApiClassesPath) ??
 631033            throw new InvalidOperationException("Failed to create runspace pool.");
 631034        if (Logger.IsEnabled(LogEventLevel.Verbose))
 1035        {
 01036            Logger.Verbose("Runspace pool created with max runspaces: {MaxRunspaces}", Options.MaxRunspaces);
 1037        }
 631038    }
 1039
 1040    /// <summary>
 1041    /// Configures the Kestrel web server with basic options.
 1042    /// </summary>
 1043    internal void ConfigureKestrelBase()
 1044    {
 611045        _ = Builder.WebHost.UseKestrel(opts =>
 611046        {
 601047            opts.CopyFromTemplate(Options.ServerOptions);
 1211048        });
 611049    }
 1050
 1051    /// <summary>
 1052    /// Configures named pipe listeners if supported on the current platform.
 1053    /// </summary>
 1054    internal void ConfigureNamedPipes()
 1055    {
 621056        if (Options.NamedPipeOptions is not null)
 1057        {
 11058            if (OperatingSystem.IsWindows())
 1059            {
 01060                _ = Builder.WebHost.UseNamedPipes(opts =>
 01061                {
 01062                    opts.ListenerQueueCount = Options.NamedPipeOptions.ListenerQueueCount;
 01063                    opts.MaxReadBufferSize = Options.NamedPipeOptions.MaxReadBufferSize;
 01064                    opts.MaxWriteBufferSize = Options.NamedPipeOptions.MaxWriteBufferSize;
 01065                    opts.CurrentUserOnly = Options.NamedPipeOptions.CurrentUserOnly;
 01066                    opts.PipeSecurity = Options.NamedPipeOptions.PipeSecurity;
 01067                });
 1068            }
 1069            else
 1070            {
 11071                Logger.Verbose("Named pipe listeners configuration is supported only on Windows; skipping UseNamedPipes 
 1072            }
 1073        }
 621074    }
 1075
 1076    /// <summary>
 1077    /// Configures HTTPS connection adapter defaults.
 1078    /// </summary>
 1079    /// <param name="serverOptions">The Kestrel server options to configure.</param>
 1080    internal void ConfigureHttpsAdapter(KestrelServerOptions serverOptions)
 1081    {
 611082        if (Options.HttpsConnectionAdapter is not null)
 1083        {
 01084            Logger.Verbose("Applying HTTPS connection adapter options from KestrunOptions.");
 1085
 1086            // Apply HTTPS defaults if needed
 01087            serverOptions.ConfigureHttpsDefaults(httpsOptions =>
 01088            {
 01089                httpsOptions.SslProtocols = Options.HttpsConnectionAdapter.SslProtocols;
 01090                httpsOptions.ClientCertificateMode = Options.HttpsConnectionAdapter.ClientCertificateMode;
 01091                httpsOptions.ClientCertificateValidation = Options.HttpsConnectionAdapter.ClientCertificateValidation;
 01092                httpsOptions.CheckCertificateRevocation = Options.HttpsConnectionAdapter.CheckCertificateRevocation;
 01093                httpsOptions.ServerCertificate = Options.HttpsConnectionAdapter.ServerCertificate;
 01094                httpsOptions.ServerCertificateChain = Options.HttpsConnectionAdapter.ServerCertificateChain;
 01095                httpsOptions.ServerCertificateSelector = Options.HttpsConnectionAdapter.ServerCertificateSelector;
 01096                httpsOptions.HandshakeTimeout = Options.HttpsConnectionAdapter.HandshakeTimeout;
 01097                httpsOptions.OnAuthenticate = Options.HttpsConnectionAdapter.OnAuthenticate;
 01098            });
 1099        }
 611100    }
 1101
 1102    /// <summary>
 1103    /// Binds all configured listeners (Unix sockets, named pipes, TCP) to the server.
 1104    /// </summary>
 1105    /// <param name="serverOptions">The Kestrel server options to configure.</param>
 1106    internal void BindListeners(KestrelServerOptions serverOptions)
 1107    {
 1108        // Unix domain socket listeners
 1241109        foreach (var unixSocket in Options.ListenUnixSockets)
 1110        {
 01111            if (!string.IsNullOrWhiteSpace(unixSocket))
 1112            {
 01113                Logger.Verbose("Binding Unix socket: {Sock}", unixSocket);
 01114                serverOptions.ListenUnixSocket(unixSocket);
 1115                // NOTE: control access via directory perms/umask; UDS file perms are inherited from process umask
 1116                // Prefer placing the socket under a group-owned dir (e.g., /var/run/kestrun) with 0770.
 1117            }
 1118        }
 1119
 1120        // Named pipe listeners
 1241121        foreach (var namedPipeName in Options.NamedPipeNames)
 1122        {
 01123            if (!string.IsNullOrWhiteSpace(namedPipeName))
 1124            {
 01125                Logger.Verbose("Binding Named Pipe: {Pipe}", namedPipeName);
 01126                serverOptions.ListenNamedPipe(namedPipeName);
 1127            }
 1128        }
 1129
 1130        // TCP listeners
 1921131        foreach (var opt in Options.Listeners)
 1132        {
 341133            serverOptions.Listen(opt.IPAddress, opt.Port, listenOptions =>
 341134            {
 341135                listenOptions.Protocols = opt.Protocols;
 341136                listenOptions.DisableAltSvcHeader = opt.DisableAltSvcHeader;
 341137                if (opt.UseHttps && opt.X509Certificate is not null)
 341138                {
 21139                    _ = listenOptions.UseHttps(opt.X509Certificate);
 341140                }
 341141                if (opt.UseConnectionLogging)
 341142                {
 01143                    _ = listenOptions.UseConnectionLogging();
 341144                }
 681145            });
 1146        }
 621147    }
 1148
 1149    /// <summary>
 1150    /// Logs the configured endpoints after building the application.
 1151    /// </summary>
 1152    internal void LogConfiguredEndpoints()
 1153    {
 1154        // build the app to validate configuration
 611155        _app = Build();
 1156        // Log configured endpoints
 611157        var dataSource = _app.Services.GetRequiredService<EndpointDataSource>();
 1158
 611159        if (dataSource.Endpoints.Count == 0)
 1160        {
 611161            Logger.Warning("EndpointDataSource is empty. No endpoints configured.");
 1162        }
 1163        else
 1164        {
 01165            foreach (var ep in dataSource.Endpoints)
 1166            {
 01167                Logger.Information("➡️  Endpoint: {DisplayName}", ep.DisplayName);
 1168            }
 1169        }
 01170    }
 1171
 1172    /// <summary>
 1173    /// Handles configuration errors and wraps them with meaningful messages.
 1174    /// </summary>
 1175    /// <param name="ex">The exception that occurred during configuration.</param>
 1176    /// <exception cref="InvalidOperationException">Always thrown with wrapped exception.</exception>
 1177    internal void HandleConfigurationError(Exception ex)
 1178    {
 11179        Logger.Error(ex, "Error applying configuration: {Message}", ex.Message);
 11180        throw new InvalidOperationException("Failed to apply configuration.", ex);
 1181    }
 1182
 1183    /// <summary>
 1184    /// Applies the configured options to the Kestrel server and initializes the runspace pool.
 1185    /// </summary>
 1186    /// <param name="userVariables">User-defined variables to inject into the runspace pool.</param>
 1187    /// <param name="userFunctions">User-defined functions to inject into the runspace pool.</param>
 1188    /// <param name="userCallbacks">User-defined callback functions for OpenAPI classes.</param>
 1189    public void EnableConfiguration(Dictionary<string, object>? userVariables = null, Dictionary<string, string>? userFu
 1190    {
 811191        if (!ValidateConfiguration())
 1192        {
 211193            return;
 1194        }
 1195
 1196        try
 1197        {
 601198            if (Logger.IsEnabled(LogEventLevel.Debug))
 1199            {
 371200                Logger.Debug("Applying configuration to KestrunHost.");
 1201            }
 1202            // Inject user variables into shared state
 601203            _ = ApplyUserVarsToState(userVariables);
 1204
 1205            // Scan for OpenAPI component annotations in the main script.
 1206            // In C#-only scenarios (including xUnit tests), there may be no PowerShell entry script.
 601207            ComponentAnnotations = !string.IsNullOrWhiteSpace(KestrunHostManager.EntryScriptPath)
 601208                && File.Exists(KestrunHostManager.EntryScriptPath)
 601209            ? OpenApiComponentAnnotationScanner.ScanFromPath(mainPath: KestrunHostManager.EntryScriptPath)
 601210            : null;
 1211
 1212            // Export OpenAPI classes from PowerShell
 601213            var openApiClassesPath = ExportOpenApiClasses(userCallbacks);
 1214            // Initialize PowerShell runspace pool
 601215            InitializeRunspacePool(userVariables: null, userFunctions: userFunctions, openApiClassesPath: openApiClasses
 1216            // Configure Kestrel server
 601217            ConfigureKestrelBase();
 1218            // Configure named pipe listeners if any
 601219            ConfigureNamedPipes();
 1220            // Normalize HTTP/3 listeners and configure QUIC if supported, or adjust listeners if QUIC is unavailable
 601221            NormalizeHttp3ListenersAndConfigureQuic();
 1222
 1223            // Apply Kestrel listeners and HTTPS settings
 601224            _ = Builder.WebHost.ConfigureKestrel(serverOptions =>
 601225            {
 601226                ConfigureHttpsAdapter(serverOptions);
 601227                BindListeners(serverOptions);
 1201228            });
 1229
 1230            // Generate OpenAPI components after runspace is ready
 1261231            foreach (var openApiDocument in OpenApiDocumentDescriptor.Values)
 1232            {
 31233                openApiDocument.GenerateComponents();
 1234            }
 1235
 1236            // Log configured endpoints after building
 601237            LogConfiguredEndpoints();
 1238
 1239            // Register default probes after endpoints are logged but before marking configured
 601240            RegisterDefaultHealthProbes();
 601241            IsConfigured = true;
 601242            Logger.Information("Configuration applied successfully.");
 601243        }
 01244        catch (Exception ex)
 1245        {
 01246            HandleConfigurationError(ex);
 01247        }
 601248    }
 1249
 1250    /// <summary>
 1251    /// Normalizes listeners that request HTTP/3 and configures QUIC when supported.
 1252    /// </summary>
 1253    /// <remarks>
 1254    /// When QUIC is unavailable, HTTP/3-only listeners fail fast with an explicit error.
 1255    /// Mixed listeners are left unchanged so Kestrel can negotiate fallback protocols.
 1256    /// </remarks>
 1257    private void NormalizeHttp3ListenersAndConfigureQuic()
 1258    {
 601259        var http3Listeners = Options.Listeners
 331260            .Where(listener => (listener.Protocols & HttpProtocols.Http3) != 0)
 601261            .ToList();
 1262
 601263        if (http3Listeners.Count == 0)
 1264        {
 601265            return;
 1266        }
 1267
 01268        if (IsQuicSupported())
 1269        {
 01270            var ports = string.Join(", ", http3Listeners.Select(listener => listener.Port));
 01271            Logger.Information("Enabling QUIC support for HTTP/3 listeners on port(s): {Ports}.", ports);
 01272            _ = Builder.WebHost.UseQuic();
 01273            return;
 1274        }
 1275
 01276        Logger.Warning("HTTP/3 was requested for {Count} listener(s), but QUIC is not supported on this platform/runtime
 1277
 01278        var http3OnlyListeners = http3Listeners
 01279            .Where(listener => listener.Protocols == HttpProtocols.Http3)
 01280            .Select(listener => listener.Port)
 01281            .ToArray();
 1282
 01283        if (http3OnlyListeners.Length > 0)
 1284        {
 01285            var ports = string.Join(", ", http3OnlyListeners);
 01286            throw new InvalidOperationException($"Unable to bind HTTP/3-only endpoint(s) on port(s): {ports}. QUIC is no
 1287        }
 1288
 01289        Logger.Information("Continuing with mixed HTTP protocol listeners unchanged; Kestrel will negotiate fallback pro
 01290    }
 1291
 1292    /// <summary>
 1293    /// Determines whether QUIC is supported by the current runtime/platform without directly calling preview-only APIs.
 1294    /// </summary>
 1295    /// <returns><c>true</c> when QUIC support is available; otherwise, <c>false</c>.</returns>
 81296    public static bool IsQuicSupported() => _isQuicSupported.Value;
 1297
 1298    /// <summary>
 1299    /// Cached reflection-based evaluator for QUIC support, using QuicListener.IsSupported.
 1300    /// </summary>
 11301    private static readonly Lazy<bool> _isQuicSupported = new(() =>
 11302    {
 11303        var quicListenerType = Type.GetType("System.Net.Quic.QuicListener, System.Net.Quic", throwOnError: false);
 11304        var isSupportedProperty = quicListenerType?.GetProperty("IsSupported", BindingFlags.Public | BindingFlags.Static
 11305        return isSupportedProperty?.GetValue(null) as bool? ?? false;
 11306    });
 1307
 1308    /// <summary>
 1309    /// Applies user-defined variables to the shared state.
 1310    /// </summary>
 1311    /// <param name="userVariables">User-defined variables to inject into the shared state.</param>
 1312    /// <returns>True if all variables were successfully applied; otherwise, false.</returns>
 1313    private bool ApplyUserVarsToState(Dictionary<string, object>? userVariables)
 1314    {
 601315        var statusSet = true;
 601316        if (userVariables is not null)
 1317        {
 41318            foreach (var v in userVariables)
 1319            {
 11320                statusSet &= SharedState.Set(v.Key, v.Value, true);
 1321            }
 1322        }
 601323        return statusSet;
 1324    }
 1325
 1326    /// <summary>
 1327    /// Exports OpenAPI classes from PowerShell.
 1328    /// </summary>
 1329    /// <param name="userCallbacks">User-defined callbacks for OpenAPI class export.</param>
 1330    private string ExportOpenApiClasses(Dictionary<string, string>? userCallbacks)
 1331    {
 1332        // Export OpenAPI classes from PowerShell
 601333        var openApiClassesPath = PowerShellOpenApiClassExporter.ExportOpenApiClasses(userCallbacks: userCallbacks);
 601334        if (Logger.IsEnabled(LogEventLevel.Debug))
 1335        {
 371336            if (string.IsNullOrWhiteSpace(openApiClassesPath))
 1337            {
 371338                Logger.Debug("No OpenAPI classes exported from PowerShell.");
 1339            }
 1340            else
 1341            {
 01342                Logger.Debug("Exported OpenAPI classes from PowerShell: {path}", openApiClassesPath);
 1343            }
 1344        }
 601345        return openApiClassesPath;
 1346    }
 1347
 1348    /// <summary>
 1349    /// Registers built-in default health probes (idempotent). Currently includes disk space probe.
 1350    /// </summary>
 1351    private void RegisterDefaultHealthProbes()
 1352    {
 1353        try
 1354        {
 1355            // Avoid duplicate registration if user already added a probe named "disk".
 601356            lock (_healthProbeLock)
 1357            {
 601358                if (HealthProbes.Any(p => string.Equals(p.Name, "disk", StringComparison.OrdinalIgnoreCase)))
 1359                {
 01360                    return; // already present
 1361                }
 601362            }
 1363
 601364            var tags = new[] { IProbe.TAG_SELF }; // neutral tag; user can filter by name if needed
 601365            var diskProbe = new DiskSpaceProbe("disk", tags);
 601366            RegisterProbeInternal(diskProbe);
 601367        }
 01368        catch (Exception ex)
 1369        {
 01370            Logger.Warning(ex, "Failed to register default disk space probe.");
 01371        }
 601372    }
 1373
 1374    #endregion
 1375    #region Builder
 1376    /* More information about the KestrunHost class
 1377    https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication?view=aspnetcore-8.0
 1378
 1379    */
 1380
 1381    /// <summary>
 1382    /// Builds the WebApplication.
 1383    /// This method applies all queued services and middleware stages,
 1384    /// and returns the built WebApplication instance.
 1385    /// </summary>
 1386    /// <returns>The built WebApplication.</returns>
 1387    /// <exception cref="InvalidOperationException"></exception>
 1388    public WebApplication Build()
 1389    {
 1101390        ValidateBuilderState();
 1101391        ApplyQueuedServices();
 1101392        BuildWebApplication();
 1101393        ConfigureBuiltInMiddleware();
 1101394        LogApplicationInfo();
 1101395        ApplyQueuedMiddleware();
 1101396        ApplyFeatures();
 1397
 1101398        return _app!;
 1399    }
 1400
 1401    /// <summary>
 1402    /// Validates that the builder is properly initialized before building.
 1403    /// </summary>
 1404    /// <exception cref="InvalidOperationException">Thrown when the builder is not initialized.</exception>
 1405    private void ValidateBuilderState()
 1406    {
 1101407        if (Builder == null)
 1408        {
 01409            throw new InvalidOperationException("Call CreateBuilder() first.");
 1410        }
 1101411    }
 1412
 1413    /// <summary>
 1414    /// Applies all queued service configurations to the service collection.
 1415    /// </summary>
 1416    private void ApplyQueuedServices()
 1417    {
 3621418        foreach (var configure in _serviceQueue)
 1419        {
 711420            configure(Builder.Services);
 1421        }
 1101422    }
 1423
 1424    /// <summary>
 1425    /// Builds the WebApplication instance from the configured builder.
 1426    /// </summary>
 1427    private void BuildWebApplication()
 1428    {
 1101429        _app = Builder.Build();
 1101430        Logger.Information("Application built successfully.");
 1431
 1101432        PopulateAppUrlsFromListeners();
 1433
 1434        // 🔔 SignalR shutdown notification
 1101435        _ = _app.Lifetime.ApplicationStopping.Register(() =>
 1101436        {
 1101437            try
 1101438            {
 241439                using var scope = _app.Services.CreateScope();
 1101440
 241441                var isService = scope.ServiceProvider.GetService<IServiceProviderIsService>();
 241442                if (isService?.IsService(typeof(IHubContext<SignalR.KestrunHub>)) != true)
 1101443                {
 241444                    Logger.Debug("SignalR hub context not available. Skipping shutdown notification.");
 241445                    return;
 1101446                }
 1101447
 01448                var hub = scope.ServiceProvider.GetRequiredService<IHubContext<SignalR.KestrunHub>>();
 01449                _ = hub.Clients.All.SendAsync("serverShutdown", "Server stopping");
 01450                Logger.Information("Sent SignalR shutdown notification to clients.");
 01451            }
 01452            catch (Exception ex)
 1101453            {
 01454                Logger.Debug(ex, "Failed to send SignalR shutdown notification.");
 01455            }
 1341456        });
 1101457    }
 1458
 1459    /// <summary>
 1460    /// Adds listener URLs to the application URL list when none are present.
 1461    /// </summary>
 1462    private void PopulateAppUrlsFromListeners()
 1463    {
 1101464        if (_app is null || _app.Urls.Count > 0)
 1465        {
 01466            return;
 1467        }
 1468
 2861469        foreach (var listener in Options.Listeners)
 1470        {
 331471            var host = listener.IPAddress == null || IPAddress.Any.Equals(listener.IPAddress) || IPAddress.IPv6Any.Equal
 331472                ? "localhost"
 331473                : listener.IPAddress.ToString();
 1474
 331475            if (listener.IPAddress != null && listener.IPAddress.AddressFamily == AddressFamily.InterNetworkV6 && !host.
 1476            {
 11477                host = $"[{host}]";
 1478            }
 1479
 331480            var scheme = listener.UseHttps ? "https" : "http";
 331481            _app.Urls.Add($"{scheme}://{host}:{listener.Port}");
 1482        }
 1101483    }
 1484
 1485    /// <summary>
 1486    /// Resolves an ephemeral port for the specified address by binding a temporary listener.
 1487    /// </summary>
 1488    /// <param name="ipAddress">The address to bind to when selecting the port.</param>
 1489    /// <returns>An available port number.</returns>
 1490    private static int ResolveEphemeralPort(IPAddress ipAddress)
 1491    {
 231492        var bindAddress = ipAddress;
 231493        if (IPAddress.Any.Equals(bindAddress))
 1494        {
 11495            bindAddress = IPAddress.Loopback;
 1496        }
 221497        else if (IPAddress.IPv6Any.Equals(bindAddress))
 1498        {
 01499            bindAddress = IPAddress.IPv6Loopback;
 1500        }
 1501
 231502        var listener = new TcpListener(bindAddress, 0);
 231503        listener.Start();
 231504        var port = ((IPEndPoint)listener.LocalEndpoint).Port;
 231505        listener.Stop();
 231506        return port;
 1507    }
 1508
 1509    /// <summary>
 1510    /// Configures built-in middleware components in the correct order.
 1511    /// </summary>
 1512    private void ConfigureBuiltInMiddleware()
 1513    {
 1514        // Configure routing
 1101515        ConfigureRouting();
 1516        // Configure CORS
 1101517        ConfigureCors();
 1518        // Configure exception handling
 1101519        ConfigureExceptionHandling();
 1520        // Configure forwarded headers
 1101521        ConfigureForwardedHeaders();
 1522        // Configure status code pages
 1101523        ConfigureStatusCodePages();
 1524        // Configure PowerShell runtime
 1101525        ConfigurePowerShellRuntime();
 1101526    }
 1527
 1528    /// <summary>
 1529    /// Configures routing middleware.
 1530    /// </summary>
 1531    private void ConfigureRouting()
 1532    {
 1101533        if (Logger.IsEnabled(LogEventLevel.Debug))
 1534        {
 821535            Logger.Debug("Enabling routing middleware.");
 1536        }
 1101537        _ = _app!.UseRouting();
 1101538        if (Logger.IsEnabled(LogEventLevel.Debug))
 1539        {
 821540            Logger.Debug("Routing middleware is enabled.");
 1541        }
 1101542    }
 1543
 1544    /// <summary>
 1545    /// Configures CORS middleware if a CORS policy is defined.
 1546    /// </summary>
 1547    private void ConfigureCors()
 1548    {
 1101549        if (CorsPolicyDefined)
 1550        {
 01551            if (Logger.IsEnabled(LogEventLevel.Debug))
 1552            {
 01553                Logger.Debug("Enabling CORS middleware.");
 1554            }
 01555            _ = _app!.UseCors();
 01556            if (Logger.IsEnabled(LogEventLevel.Debug))
 1557            {
 01558                Logger.Debug("CORS middleware is enabled.");
 1559            }
 1560        }
 1101561    }
 1562
 1563    /// <summary>
 1564    /// Configures exception handling middleware if enabled.
 1565    /// </summary>
 1566    private void ConfigureExceptionHandling()
 1567    {
 1101568        if (ExceptionOptions is not null)
 1569        {
 51570            if (Logger.IsEnabled(LogEventLevel.Debug))
 1571            {
 01572                Logger.Debug("Enabling exception handling middleware.");
 1573            }
 51574            _ = ExceptionOptions.DeveloperExceptionPageOptions is not null
 51575                ? _app!.UseDeveloperExceptionPage(ExceptionOptions.DeveloperExceptionPageOptions)
 51576                : _app!.UseExceptionHandler(ExceptionOptions);
 51577            if (Logger.IsEnabled(LogEventLevel.Debug))
 1578            {
 01579                Logger.Debug("Exception handling middleware is enabled.");
 1580            }
 1581        }
 1101582    }
 1583
 1584    /// <summary>
 1585    /// Configures forwarded headers middleware if enabled.
 1586    /// </summary>
 1587    private void ConfigureForwardedHeaders()
 1588    {
 1101589        if (ForwardedHeaderOptions is not null)
 1590        {
 31591            if (Logger.IsEnabled(LogEventLevel.Debug))
 1592            {
 01593                Logger.Debug("Enabling forwarded headers middleware.");
 1594            }
 31595            _ = _app!.UseForwardedHeaders(ForwardedHeaderOptions);
 31596            if (Logger.IsEnabled(LogEventLevel.Debug))
 1597            {
 01598                Logger.Debug("Forwarded headers middleware is enabled.");
 1599            }
 1600        }
 1101601    }
 1602
 1603    /// <summary>
 1604    /// Configures status code pages middleware if enabled.
 1605    /// </summary>
 1606    private void ConfigureStatusCodePages()
 1607    {
 1608        // Register StatusCodePages BEFORE language runtimes so that re-executed requests
 1609        // pass through language middleware again (and get fresh RouteValues/context).
 1101610        if (StatusCodeOptions is not null)
 1611        {
 01612            if (Logger.IsEnabled(LogEventLevel.Debug))
 1613            {
 01614                Logger.Debug("Enabling status code pages middleware.");
 1615            }
 01616            _ = _app!.UseStatusCodePages(StatusCodeOptions);
 01617            if (Logger.IsEnabled(LogEventLevel.Debug))
 1618            {
 01619                Logger.Debug("Status code pages middleware is enabled.");
 1620            }
 1621        }
 1101622    }
 1623
 1624    /// <summary>
 1625    /// Configures PowerShell runtime middleware if enabled.
 1626    /// </summary>
 1627    /// <exception cref="InvalidOperationException">Thrown when PowerShell is enabled but runspace pool is not initializ
 1628    private void ConfigurePowerShellRuntime()
 1629    {
 1101630        if (PowershellMiddlewareEnabled)
 1631        {
 01632            if (Logger.IsEnabled(LogEventLevel.Debug))
 1633            {
 01634                Logger.Debug("Enabling PowerShell middleware.");
 1635            }
 1636
 01637            if (_runspacePool is null)
 1638            {
 01639                throw new InvalidOperationException("Runspace pool is not initialized. Call EnableConfiguration first.")
 1640            }
 1641
 01642            Logger.Information("Adding PowerShell runtime");
 01643            _ = _app!.UseLanguageRuntime(
 01644                    ScriptLanguage.PowerShell,
 01645                    b => b.UsePowerShellRunspace(_runspacePool));
 1646
 01647            if (Logger.IsEnabled(LogEventLevel.Debug))
 1648            {
 01649                Logger.Debug("PowerShell middleware is enabled.");
 1650            }
 1651        }
 1101652    }
 1653
 1654    /// <summary>
 1655    /// Logs application information including working directory and Pages directory contents.
 1656    /// </summary>
 1657    private void LogApplicationInfo()
 1658    {
 1101659        Logger.Information("CWD: {CWD}", GetSafeCurrentDirectory());
 1101660        Logger.Information("ContentRoot: {Root}", _app!.Environment.ContentRootPath);
 1101661        LogPagesDirectory();
 1101662    }
 1663
 1664    /// <summary>
 1665    /// Logs information about the Pages directory and its contents.
 1666    /// </summary>
 1667    private void LogPagesDirectory()
 1668    {
 1101669        var pagesDir = Path.Combine(_app!.Environment.ContentRootPath, "Pages");
 1101670        Logger.Information("Pages Dir: {PagesDir}", pagesDir);
 1671
 1101672        if (Directory.Exists(pagesDir))
 1673        {
 21674            foreach (var file in Directory.GetFiles(pagesDir, "*.*", SearchOption.AllDirectories))
 1675            {
 01676                Logger.Information("Pages file: {File}", file);
 1677            }
 1678        }
 1679        else
 1680        {
 1091681            Logger.Warning("Pages directory does not exist: {PagesDir}", pagesDir);
 1682        }
 1091683    }
 1684
 1685    /// <summary>
 1686    /// Applies all queued middleware stages to the application pipeline.
 1687    /// </summary>
 1688    private void ApplyQueuedMiddleware()
 1689    {
 3141690        foreach (var stage in _middlewareQueue)
 1691        {
 471692            stage(_app!);
 1693        }
 1101694    }
 1695
 1696    /// <summary>
 1697    /// Applies all queued features to the host.
 1698    /// </summary>
 1699    private void ApplyFeatures()
 1700    {
 2241701        foreach (var feature in FeatureQueue)
 1702        {
 21703            feature(this);
 1704        }
 1101705    }
 1706
 1707    /// <summary>
 1708    /// Returns true if the specified service type has already been registered in the IServiceCollection.
 1709    /// </summary>
 1710    public bool IsServiceRegistered(Type serviceType)
 7981711        => Builder?.Services?.Any(sd => sd.ServiceType == serviceType) ?? false;
 1712
 1713    /// <summary>
 1714    /// Generic convenience overload.
 1715    /// </summary>
 01716    public bool IsServiceRegistered<TService>() => IsServiceRegistered(typeof(TService));
 1717
 1718    /// <summary>
 1719    /// Adds a service configuration action to the service queue.
 1720    /// This action will be executed when the services are built.
 1721    /// </summary>
 1722    /// <param name="configure">The service configuration action.</param>
 1723    /// <returns>The current KestrunHost instance.</returns>
 1724    public KestrunHost AddService(Action<IServiceCollection> configure)
 1725    {
 1341726        _serviceQueue.Add(configure);
 1341727        return this;
 1728    }
 1729
 1730    /// <summary>
 1731    /// Adds a middleware stage to the application pipeline.
 1732    /// </summary>
 1733    /// <param name="stage">The middleware stage to add.</param>
 1734    /// <returns>The current KestrunHost instance.</returns>
 1735    public KestrunHost Use(Action<IApplicationBuilder> stage)
 1736    {
 1091737        _middlewareQueue.Add(stage);
 1091738        return this;
 1739    }
 1740
 1741    /// <summary>
 1742    /// Adds a feature configuration action to the feature queue.
 1743    /// This action will be executed when the features are applied.
 1744    /// </summary>
 1745    /// <param name="feature">The feature configuration action.</param>
 1746    /// <returns>The current KestrunHost instance.</returns>
 1747    public KestrunHost AddFeature(Action<KestrunHost> feature)
 1748    {
 21749        FeatureQueue.Add(feature);
 21750        return this;
 1751    }
 1752
 1753    /// <summary>
 1754    /// Adds a scheduling feature to the Kestrun host, optionally specifying the maximum number of runspaces for the sch
 1755    /// </summary>
 1756    /// <param name="MaxRunspaces">The maximum number of runspaces for the scheduler. If null, uses the default value.</
 1757    /// <returns>The current KestrunHost instance.</returns>
 1758    public KestrunHost AddScheduling(int? MaxRunspaces = null)
 1759    {
 41760        return MaxRunspaces is not null and <= 0
 41761            ? throw new ArgumentOutOfRangeException(nameof(MaxRunspaces), "MaxRunspaces must be greater than zero.")
 41762            : AddFeature(host =>
 41763        {
 21764            if (Logger.IsEnabled(LogEventLevel.Debug))
 41765            {
 21766                Logger.Debug("AddScheduling (deferred)");
 41767            }
 41768
 21769            if (host._scheduler is null)
 41770            {
 11771                if (MaxRunspaces is not null and > 0)
 41772                {
 11773                    Logger.Information("Setting MaxSchedulerRunspaces to {MaxRunspaces}", MaxRunspaces);
 11774                    host.Options.MaxSchedulerRunspaces = MaxRunspaces.Value;
 41775                }
 11776                Logger.Verbose("Creating SchedulerService with MaxSchedulerRunspaces={MaxRunspaces}",
 11777                    host.Options.MaxSchedulerRunspaces);
 11778                var pool = host.CreateRunspacePool(host.Options.MaxSchedulerRunspaces);
 11779                var logger = Logger.ForContext<KestrunHost>();
 11780                host.Scheduler = new SchedulerService(pool, logger);
 41781            }
 41782            else
 41783            {
 11784                Logger.Warning("SchedulerService already configured; skipping.");
 41785            }
 51786        });
 1787    }
 1788
 1789    /// <summary>
 1790    /// Adds the Tasks feature to run ad-hoc scripts with status/result/cancellation.
 1791    /// </summary>
 1792    /// <param name="MaxRunspaces">Optional max runspaces for the task PowerShell pool; when null uses scheduler default
 1793    /// <returns>The current KestrunHost instance.</returns>
 1794    public KestrunHost AddTasks(int? MaxRunspaces = null)
 1795    {
 01796        return MaxRunspaces is not null and <= 0
 01797            ? throw new ArgumentOutOfRangeException(nameof(MaxRunspaces), "MaxRunspaces must be greater than zero.")
 01798            : AddFeature(host =>
 01799        {
 01800            if (Logger.IsEnabled(LogEventLevel.Debug))
 01801            {
 01802                Logger.Debug("AddTasks (deferred)");
 01803            }
 01804
 01805            if (host._tasks is null)
 01806            {
 01807                // Reuse scheduler pool sizing unless explicitly overridden
 01808                if (MaxRunspaces is not null and > 0)
 01809                {
 01810                    Logger.Information("Setting MaxTaskRunspaces to {MaxRunspaces}", MaxRunspaces);
 01811                }
 01812                var pool = host.CreateRunspacePool(MaxRunspaces ?? host.Options.MaxSchedulerRunspaces);
 01813                var logger = Logger.ForContext<KestrunHost>();
 01814                host.Tasks = new KestrunTaskService(pool, logger);
 01815            }
 01816            else
 01817            {
 01818                Logger.Warning("KestrunTaskService already configured; skipping.");
 01819            }
 01820        });
 1821    }
 1822
 1823    /// <summary>
 1824    /// Adds MVC / API controllers to the application.
 1825    /// </summary>
 1826    /// <param name="cfg">The configuration options for MVC / API controllers.</param>
 1827    /// <returns>The current KestrunHost instance.</returns>
 1828    public KestrunHost AddControllers(Action<Microsoft.AspNetCore.Mvc.MvcOptions>? cfg = null)
 1829    {
 01830        return AddService(services =>
 01831        {
 01832            var builder = services.AddControllers();
 01833            if (cfg != null)
 01834            {
 01835                _ = builder.ConfigureApplicationPartManager(pm => { }); // customise if you wish
 01836            }
 01837        });
 1838    }
 1839
 1840    /// <summary>
 1841    /// Adds a PowerShell runtime to the application.
 1842    /// This middleware allows you to execute PowerShell scripts in response to HTTP requests.
 1843    /// </summary>
 1844    /// <param name="routePrefix">The route prefix to use for the PowerShell runtime.</param>
 1845    /// <returns>The current KestrunHost instance.</returns>
 1846    public KestrunHost AddPowerShellRuntime(PathString? routePrefix = null)
 1847    {
 11848        if (Logger.IsEnabled(LogEventLevel.Debug))
 1849        {
 11850            Logger.Debug("Adding PowerShell runtime with route prefix: {RoutePrefix}", routePrefix);
 1851        }
 1852
 11853        return Use(app =>
 11854        {
 11855            ArgumentNullException.ThrowIfNull(_runspacePool);
 11856            // ── mount PowerShell at the root ──
 11857            _ = app.UseLanguageRuntime(
 11858                ScriptLanguage.PowerShell,
 21859                b => b.UsePowerShellRunspace(_runspacePool));
 21860        });
 1861    }
 1862
 1863    /// <summary>
 1864    /// Adds the Realtime tag to the OpenAPI document if not already present.
 1865    /// </summary>
 1866    /// <param name="defTag"> OpenAPI document descriptor to which the Realtime tag will be added.</param>
 1867    private static void AddRealTimeTag(OpenApiDocDescriptor defTag)
 1868    {
 1869        // Add Realtime default tag if not present
 21870        if (!defTag.ContainsTag("Realtime"))
 1871        {
 21872            _ = defTag.AddTag(name: "Realtime",
 21873                summary: "Real-time communication",
 21874                description: "Protocols and endpoints for real-time, push-based communication such as SignalR and Server
 21875                kind: "nav",
 21876                externalDocs: new OpenApiExternalDocs
 21877                {
 21878                    Description = "Real-time communication overview",
 21879                    Url = new Uri("https://learn.microsoft.com/aspnet/core/signalr/")
 21880                });
 1881        }
 21882    }
 1883
 1884    /// <summary>
 1885    /// Adds the SignalR tag to the OpenAPI document if not already present.
 1886    /// </summary>
 1887    /// <param name="defTag"> OpenAPI document descriptor to which the SignalR tag will be added.</param>
 1888    private static void AddSignalRTag(OpenApiDocDescriptor defTag)
 1889    {
 01890        if (!defTag.ContainsTag(SignalROptions.DefaultTag))
 1891        {
 01892            _ = defTag.AddTag(name: SignalROptions.DefaultTag,
 01893                 description: "SignalR hubs providing real-time, bidirectional communication over persistent connections
 01894                 summary: "SignalR hubs",
 01895                 parent: "Realtime",
 01896                  externalDocs: new OpenApiExternalDocs
 01897                  {
 01898                      Description = "ASP.NET Core SignalR documentation",
 01899                      Url = new Uri("https://learn.microsoft.com/aspnet/core/signalr/introduction")
 01900                  });
 1901        }
 01902    }
 1903
 1904    /// <summary>
 1905    /// Computes the SignalR negotiate endpoint path based on the hub path.
 1906    /// </summary>
 1907    /// <param name="hubPath">The hub route path.</param>
 1908    /// <returns>The negotiate path for the hub.</returns>
 1909    private static string GetSignalRNegotiatePath(string hubPath)
 01910        => hubPath.EndsWith("/negotiate", StringComparison.OrdinalIgnoreCase)
 01911            ? hubPath
 01912            : hubPath.TrimEnd('/') + "/negotiate";
 1913
 1914    /// <summary>
 1915    /// Creates a native route registration with no script body.
 1916    /// </summary>
 1917    /// <param name="pattern">The route pattern.</param>
 1918    /// <param name="verb">The HTTP verb for the route.</param>
 1919    /// <returns>A configured <see cref="MapRouteOptions"/> instance.</returns>
 1920    private static MapRouteOptions CreateNativeRouteOptions(string pattern, HttpVerb verb)
 01921        => new()
 01922        {
 01923            Pattern = pattern,
 01924            HttpVerbs = [verb],
 01925            ScriptCode = new LanguageOptions
 01926            {
 01927                Language = ScriptLanguage.Native,
 01928                Code = string.Empty
 01929            }
 01930        };
 1931
 1932    /// <summary>
 1933    /// Registers a route in the internal route registry.
 1934    /// </summary>
 1935    /// <param name="pattern">The route pattern.</param>
 1936    /// <param name="verb">The HTTP verb.</param>
 1937    /// <param name="routeOptions">The route options.</param>
 1938    private void RegisterRoute(string pattern, HttpVerb verb, MapRouteOptions routeOptions)
 01939        => _registeredRoutes[(pattern, verb)] = routeOptions;
 1940
 1941    /// <summary>
 1942    /// Ensures the default OpenAPI tags for real-time and SignalR are present when the caller uses default tagging.
 1943    /// </summary>
 1944    /// <param name="options">SignalR configuration options.</param>
 1945    /// <param name="apiDocDescriptors">OpenAPI document descriptors to update.</param>
 1946    private static void EnsureDefaultSignalRTags(SignalROptions options, IEnumerable<OpenApiDocDescriptor> apiDocDescrip
 1947    {
 01948        if (options.Tags?.Contains(SignalROptions.DefaultTag) != true)
 1949        {
 01950            return;
 1951        }
 1952
 01953        foreach (var defTag in apiDocDescriptors)
 1954        {
 01955            AddRealTimeTag(defTag);
 01956            AddSignalRTag(defTag);
 1957        }
 01958    }
 1959
 1960    /// <summary>
 1961    /// Creates the common OpenAPI response set for the SignalR hub connect endpoint.
 1962    /// </summary>
 1963    /// <returns>The OpenAPI responses collection.</returns>
 1964    private static OpenApiResponses CreateSignalRHubResponses()
 01965        => new()
 01966        {
 01967            ["101"] = new OpenApiResponse { Description = "Switching Protocols (WebSocket upgrade)" },
 01968            ["401"] = new OpenApiResponse { Description = "Unauthorized" },
 01969            ["403"] = new OpenApiResponse { Description = "Forbidden" },
 01970            ["404"] = new OpenApiResponse { Description = "Not Found" },
 01971            ["500"] = new OpenApiResponse { Description = "Internal Server Error" }
 01972        };
 1973
 1974    /// <summary>
 1975    /// Creates the common OpenAPI response set for the SignalR negotiate endpoint.
 1976    /// </summary>
 1977    /// <returns>The OpenAPI responses collection.</returns>
 1978    private static OpenApiResponses CreateSignalRNegotiateResponses()
 01979        => new()
 01980        {
 01981            ["200"] = new OpenApiResponse { Description = "Successful negotiation" },
 01982            ["401"] = new OpenApiResponse { Description = "Unauthorized" },
 01983            ["403"] = new OpenApiResponse { Description = "Forbidden" },
 01984            ["404"] = new OpenApiResponse { Description = "Not Found" },
 01985            ["500"] = new OpenApiResponse { Description = "Internal Server Error" }
 01986        };
 1987
 1988    /// <summary>
 1989    /// Builds the OpenAPI extensions for SignalR endpoints.
 1990    /// </summary>
 1991    /// <param name="options">SignalR configuration options.</param>
 1992    /// <param name="negotiatePath">The negotiate endpoint path.</param>
 1993    /// <param name="role">The SignalR endpoint role (e.g., connect, negotiate).</param>
 1994    /// <returns>Extensions dictionary for OpenAPI metadata.</returns>
 1995    private static Dictionary<string, IOpenApiExtension> CreateSignalRExtensions(SignalROptions options, string negotiat
 01996        => new()
 01997        {
 01998            ["x-signalr-role"] = new JsonNodeExtension(JsonValue.Create(role)),
 01999            ["x-signalr"] = new JsonNodeExtension(new JsonObject
 02000            {
 02001                ["hub"] = options.HubName,
 02002                ["path"] = options.Path,
 02003                ["negotiatePath"] = negotiatePath,
 02004                ["connectOperation"] = "get:" + options.Path,
 02005                ["transports"] = new JsonArray("websocket", "sse", "longPolling"),
 02006                ["formats"] = new JsonArray("json"),
 02007            })
 02008        };
 2009
 2010    /// <summary>
 2011    /// Adds OpenAPI metadata to the hub connect route, if OpenAPI is enabled.
 2012    /// </summary>
 2013    /// <param name="options">SignalR configuration options.</param>
 2014    /// <param name="apiDocDescriptors">OpenAPI document descriptors for tag registration.</param>
 2015    /// <param name="routeOptions">The route options to enrich with OpenAPI metadata.</param>
 2016    /// <param name="negotiatePath">The computed negotiate endpoint path.</param>
 2017    private void TryAddSignalRHubOpenApiMetadata(
 2018        SignalROptions options,
 2019        IEnumerable<OpenApiDocDescriptor> apiDocDescriptors,
 2020        MapRouteOptions routeOptions,
 2021        string negotiatePath)
 2022    {
 02023        if (options.SkipOpenApi)
 2024        {
 02025            return;
 2026        }
 2027
 02028        if (Logger.IsEnabled(LogEventLevel.Debug))
 2029        {
 02030            Logger.Debug("Adding OpenAPI metadata for SignalR hub at path: {Path}", options.Path);
 2031        }
 2032
 02033        EnsureDefaultSignalRTags(options, apiDocDescriptors);
 2034
 02035        var meta = new OpenAPIPathMetadata(pattern: options.Path, mapOptions: routeOptions)
 02036        {
 02037            DocumentId = options.DocId,
 02038            Summary = string.IsNullOrWhiteSpace(options.Summary) ? null : options.Summary,
 02039            Description = string.IsNullOrWhiteSpace(options.Description) ? null : options.Description,
 02040            Tags = options.Tags?.ToList() ?? [],
 02041            Responses = CreateSignalRHubResponses(),
 02042            Extensions = CreateSignalRExtensions(options, negotiatePath, role: "connect")
 02043        };
 2044
 02045        routeOptions.OpenAPI[HttpVerb.Get] = meta;
 02046    }
 2047
 2048    /// <summary>
 2049    /// Adds OpenAPI metadata to the negotiate route, if OpenAPI is enabled.
 2050    /// </summary>
 2051    /// <param name="options">SignalR configuration options.</param>
 2052    /// <param name="negotiateRouteOptions">The negotiate route options to enrich with OpenAPI metadata.</param>
 2053    /// <param name="negotiatePath">The negotiate endpoint path.</param>
 2054    private static void TryAddSignalRNegotiateOpenApiMetadata(
 2055        SignalROptions options,
 2056        MapRouteOptions negotiateRouteOptions,
 2057        string negotiatePath)
 2058    {
 02059        if (options.SkipOpenApi)
 2060        {
 02061            return;
 2062        }
 2063
 02064        var negotiateMeta = new OpenAPIPathMetadata(pattern: negotiatePath, mapOptions: negotiateRouteOptions)
 02065        {
 02066            Summary = "SignalR negotiate endpoint",
 02067            Description = "Negotiates connection parameters for a SignalR client before establishing the transport.",
 02068            Tags = options.Tags?.ToList() ?? [],
 02069            Responses = CreateSignalRNegotiateResponses(),
 02070            Extensions = CreateSignalRExtensions(options, negotiatePath, role: "negotiate")
 02071        };
 2072
 02073        negotiateRouteOptions.OpenAPI[HttpVerb.Post] = negotiateMeta;
 02074    }
 2075
 2076    /// <summary>
 2077    /// Registers SignalR services and JSON protocol configuration.
 2078    /// </summary>
 2079    /// <typeparam name="THub">The hub type being registered.</typeparam>
 2080    /// <param name="services">The service collection to configure.</param>
 2081    private static void ConfigureSignalRServices<THub>(IServiceCollection services) where THub : Hub
 2082    {
 02083        _ = services.AddSignalR(o =>
 02084        {
 02085            o.HandshakeTimeout = TimeSpan.FromSeconds(5);
 02086            o.KeepAliveInterval = TimeSpan.FromSeconds(2);
 02087            o.ClientTimeoutInterval = TimeSpan.FromSeconds(10);
 02088        }).AddJsonProtocol(opts =>
 02089        {
 02090            // Avoid failures when payloads contain cycles; our sanitizer should prevent most, this is a safety net.
 02091            opts.PayloadSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
 02092        });
 2093
 2094        // Register IRealtimeBroadcaster as singleton if it's the KestrunHub
 02095        if (typeof(THub) == typeof(SignalR.KestrunHub))
 2096        {
 02097            _ = services.AddSingleton<SignalR.IRealtimeBroadcaster, SignalR.RealtimeBroadcaster>();
 02098            _ = services.AddSingleton<SignalR.IConnectionTracker, SignalR.InMemoryConnectionTracker>();
 2099        }
 02100    }
 2101
 2102    /// <summary>
 2103    /// Maps the SignalR hub to the application's endpoint route builder.
 2104    /// </summary>
 2105    /// <typeparam name="THub">The hub type being mapped.</typeparam>
 2106    /// <param name="app">The application builder.</param>
 2107    /// <param name="path">The hub path.</param>
 2108    private static void MapSignalRHub<THub>(IApplicationBuilder app, string path) where THub : Hub
 02109        => ((IEndpointRouteBuilder)app).MapHub<THub>(path);
 2110
 2111    /// <summary>
 2112    /// Adds a SignalR hub to the application at the specified path.
 2113    /// </summary>
 2114    /// <typeparam name="T">The type of the SignalR hub.</typeparam>
 2115    /// <param name="options">The options for configuring the SignalR hub.</param>
 2116    /// <returns>The current KestrunHost instance.</returns>
 2117    public KestrunHost AddSignalR<T>(SignalROptions options) where T : Hub
 2118    {
 02119        options ??= SignalROptions.Default;
 2120
 02121        var apiDocDescriptors = GetOrCreateOpenApiDocument(options.DocId);
 02122        var negotiatePath = GetSignalRNegotiatePath(options.Path);
 2123
 02124        var routeOptions = CreateNativeRouteOptions(options.Path, HttpVerb.Get);
 02125        TryAddSignalRHubOpenApiMetadata(options, apiDocDescriptors, routeOptions, negotiatePath);
 02126        RegisterRoute(options.Path, HttpVerb.Get, routeOptions);
 2127
 02128        if (options.IncludeNegotiateEndpoint)
 2129        {
 02130            var negotiateRouteOptions = CreateNativeRouteOptions(negotiatePath, HttpVerb.Post);
 02131            TryAddSignalRNegotiateOpenApiMetadata(options, negotiateRouteOptions, negotiatePath);
 02132            RegisterRoute(negotiatePath, HttpVerb.Post, negotiateRouteOptions);
 2133        }
 2134
 02135        if (Logger.IsEnabled(LogEventLevel.Debug))
 2136        {
 02137            Logger.Debug("Adding SignalR hub of type {HubType} at path: {Path}", typeof(T).FullName, options.Path);
 2138        }
 2139
 02140        return AddService(ConfigureSignalRServices<T>)
 02141            .Use(app => MapSignalRHub<T>(app, options.Path));
 2142    }
 2143
 2144    /// <summary>
 2145    /// Adds the default SignalR hub (KestrunHub) to the application at the specified path.
 2146    /// </summary>
 2147    /// <param name="options">The options for configuring the SignalR hub.</param>
 2148    /// <returns></returns>
 02149    public KestrunHost AddSignalR(SignalROptions options) => AddSignalR<SignalR.KestrunHub>(options);
 2150
 2151    /*
 2152        // ④ gRPC
 2153        public KestrunHost AddGrpc<TService>() where TService : class
 2154        {
 2155            return AddService(s => s.AddGrpc())
 2156                   .Use(app => app.MapGrpcService<TService>());
 2157        }
 2158    */
 2159
 2160    // Add as many tiny helpers as you wish:
 2161    // • AddAuthentication(jwt => { … })
 2162    // • AddSignalR()
 2163    // • AddHealthChecks()
 2164    // • AddGrpc()
 2165    // etc.
 2166
 2167    #endregion
 2168    #region Run/Start/Stop
 2169
 2170    /// <summary>
 2171    /// Runs the Kestrun web application, applying configuration and starting the server.
 2172    /// </summary>
 2173    public void Run()
 2174    {
 02175        if (Logger.IsEnabled(LogEventLevel.Debug))
 2176        {
 02177            Logger.Debug("Run() called");
 2178        }
 2179
 02180        EnableConfiguration();
 02181        Runtime.StartTime = DateTime.UtcNow;
 02182        _app?.Run();
 02183    }
 2184
 2185    /// <summary>
 2186    /// Starts the Kestrun web application asynchronously.
 2187    /// </summary>
 2188    /// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
 2189    /// <returns>A task that represents the asynchronous start operation.</returns>
 2190    public async Task StartAsync(CancellationToken cancellationToken = default)
 2191    {
 212192        if (Logger.IsEnabled(LogEventLevel.Debug))
 2193        {
 12194            Logger.Debug("StartAsync() called");
 2195        }
 2196
 212197        EnableConfiguration();
 212198        if (_app != null)
 2199        {
 212200            Runtime.StartTime = DateTime.UtcNow;
 212201            await _app.StartAsync(cancellationToken);
 2202        }
 212203    }
 2204
 2205    /// <summary>
 2206    /// Stops the Kestrun web application asynchronously.
 2207    /// </summary>
 2208    /// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
 2209    /// <returns>A task that represents the asynchronous stop operation.</returns>
 2210    public async Task StopAsync(CancellationToken cancellationToken = default)
 2211    {
 262212        if (Logger.IsEnabled(LogEventLevel.Debug))
 2213        {
 62214            Logger.Debug("StopAsync() called");
 2215        }
 2216
 262217        if (_app != null)
 2218        {
 2219            try
 2220            {
 2221                // Initiate graceful shutdown
 212222                await _app.StopAsync(cancellationToken);
 212223                Runtime.StopTime = DateTime.UtcNow;
 212224            }
 02225            catch (Exception ex) when (ex.GetType().FullName == "System.Net.Quic.QuicException")
 2226            {
 2227                // QUIC exceptions can occur during shutdown, especially if the server is not using QUIC.
 2228                // We log this as a debug message to avoid cluttering the logs with expected exceptions.
 2229                // This is a workaround for
 2230
 02231                Logger.Debug("Ignored QUIC exception during shutdown: {Message}", ex.Message);
 02232            }
 2233        }
 262234    }
 2235
 2236    /// <summary>
 2237    /// Initiates a graceful shutdown of the Kestrun web application.
 2238    /// </summary>
 2239    public void Stop()
 2240    {
 12241        if (Interlocked.Exchange(ref _stopping, 1) == 1)
 2242        {
 02243            return; // already stopping
 2244        }
 12245        if (Logger.IsEnabled(LogEventLevel.Debug))
 2246        {
 12247            Logger.Debug("Stop() called");
 2248        }
 2249        // This initiates a graceful shutdown.
 12250        _app?.Lifetime.StopApplication();
 12251        Runtime.StopTime = DateTime.UtcNow;
 12252    }
 2253
 2254    /// <summary>
 2255    /// Determines whether the Kestrun web application is currently running.
 2256    /// </summary>
 2257    /// <returns>True if the application is running; otherwise, false.</returns>
 2258    public bool IsRunning
 2259    {
 2260        get
 2261        {
 82262            var appField = typeof(KestrunHost)
 82263                .GetField("_app", BindingFlags.NonPublic | BindingFlags.Instance);
 2264
 82265            return appField?.GetValue(this) is WebApplication app && !app.Lifetime.ApplicationStopping.IsCancellationReq
 2266        }
 2267    }
 2268
 2269    #endregion
 2270
 2271    #region Runspace Pool Management
 2272
 2273    /// <summary>
 2274    /// Creates and returns a new <see cref="KestrunRunspacePoolManager"/> instance with the specified maximum number of
 2275    /// </summary>
 2276    /// <param name="maxRunspaces">The maximum number of runspaces to create. If not specified or zero, defaults to twic
 2277    /// <param name="userVariables">A dictionary of user-defined variables to inject into the runspace pool.</param>
 2278    /// <param name="userFunctions">A dictionary of user-defined functions to inject into the runspace pool.</param>
 2279    /// <param name="openApiClassesPath">The file path to the OpenAPI class definitions to inject into the runspace pool
 2280    /// <returns>A configured <see cref="KestrunRunspacePoolManager"/> instance.</returns>
 2281    public KestrunRunspacePoolManager CreateRunspacePool(int? maxRunspaces = 0, Dictionary<string, object>? userVariable
 2282    {
 692283        LogCreateRunspacePool(maxRunspaces);
 2284
 692285        var iss = BuildInitialSessionState(openApiClassesPath);
 692286        AddHostVariables(iss);
 692287        AddSharedVariables(iss);
 692288        AddUserVariables(iss, userVariables);
 692289        AddUserFunctions(iss, userFunctions);
 2290
 692291        var maxRs = ResolveMaxRunspaces(maxRunspaces);
 2292
 692293        Logger.Information("Creating runspace pool with max runspaces: {MaxRunspaces}", maxRs);
 692294        return new KestrunRunspacePoolManager(this, Options?.MinRunspaces ?? 1, maxRunspaces: maxRs, initialSessionState
 2295    }
 2296
 2297    private void LogCreateRunspacePool(int? maxRunspaces)
 2298    {
 692299        if (Logger.IsEnabled(LogEventLevel.Debug))
 2300        {
 462301            Logger.Debug("CreateRunspacePool() called: {@MaxRunspaces}", maxRunspaces);
 2302        }
 692303    }
 2304
 2305    private InitialSessionState BuildInitialSessionState(string? openApiClassesPath)
 2306    {
 692307        var iss = InitialSessionState.CreateDefault();
 2308
 692309        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
 2310        {
 2311            // On Windows, we can use the full .NET Framework modules
 02312            iss.ExecutionPolicy = ExecutionPolicy.Unrestricted;
 2313        }
 2314
 692315        ImportModulePaths(iss);
 692316        AddOpenApiStartupScript(iss, openApiClassesPath);
 2317
 692318        return iss;
 2319    }
 2320
 2321    private void ImportModulePaths(InitialSessionState iss)
 2322    {
 2762323        foreach (var path in _modulePaths)
 2324        {
 692325            iss.ImportPSModule([path]);
 2326        }
 692327    }
 2328
 2329    private void AddOpenApiStartupScript(InitialSessionState iss, string? openApiClassesPath)
 2330    {
 692331        if (string.IsNullOrWhiteSpace(openApiClassesPath))
 2332        {
 682333            return;
 2334        }
 2335
 12336        _ = iss.StartupScripts.Add(openApiClassesPath);
 12337        if (Logger.IsEnabled(LogEventLevel.Debug))
 2338        {
 12339            Logger.Debug("Configured OpenAPI class script at {ScriptPath}", openApiClassesPath);
 2340        }
 12341    }
 2342
 2343    private void AddHostVariables(InitialSessionState iss)
 2344    {
 692345        iss.Variables.Add(
 692346            new SessionStateVariableEntry(
 692347                "KrServer",
 692348                this,
 692349                "The Kestrun Server Host (KestrunHost) instance"
 692350            )
 692351        );
 692352    }
 2353
 2354    private void AddSharedVariables(InitialSessionState iss)
 2355    {
 1402356        foreach (var kvp in SharedState.Snapshot())
 2357        {
 12358            iss.Variables.Add(
 12359                new SessionStateVariableEntry(
 12360                    kvp.Key,
 12361                    kvp.Value,
 12362                    "Global variable"
 12363                )
 12364            );
 2365        }
 692366    }
 2367
 2368    private static void AddUserVariables(InitialSessionState iss, IReadOnlyDictionary<string, object>? userVariables)
 2369    {
 692370        if (userVariables is null)
 2371        {
 672372            return;
 2373        }
 2374
 102375        foreach (var kvp in userVariables)
 2376        {
 32377            if (kvp.Value is PSVariable psVar)
 2378            {
 12379                iss.Variables.Add(
 12380                    new SessionStateVariableEntry(
 12381                        kvp.Key,
 12382                        UnwrapKestrunVariableValue(psVar.Value),
 12383                        psVar.Description ?? "User-defined variable"
 12384                    )
 12385                );
 12386                continue;
 2387            }
 2388
 22389            iss.Variables.Add(
 22390                new SessionStateVariableEntry(
 22391                    kvp.Key,
 22392                    UnwrapKestrunVariableValue(kvp.Value),
 22393                    "User-defined variable"
 22394                )
 22395            );
 2396        }
 22397    }
 2398
 2399    /// <summary>
 2400    /// Unwraps a Kestrun variable value if it is wrapped in a dictionary with a specific marker.
 2401    /// </summary>
 2402    /// <param name="raw">The raw variable value to unwrap.</param>
 2403    /// <returns>The unwrapped variable value, or the original value if not wrapped.</returns>
 2404    private static object? UnwrapKestrunVariableValue(object? raw)
 2405    {
 32406        if (raw is null)
 2407        {
 02408            return null;
 2409        }
 2410
 2411        // unwrap PSObject if needed
 32412        raw = UnwrapPsObject(raw);
 2413
 2414        // check for dictionary
 32415        if (raw is not System.Collections.IDictionary dict)
 2416        {
 32417            return raw;
 2418        }
 2419
 2420        // check for marker key
 02421        if (!TryGetDictionaryValueIgnoreCase(dict, KestrunVariableMarkerKey, out var markerObj))
 2422        {
 02423            return raw;
 2424        }
 2425
 2426        // check if marker is enabled
 02427        if (!IsKestrunVariableMarkerEnabled(markerObj))
 2428        {
 02429            return raw;
 2430        }
 2431
 2432        // extract the "Value" entry
 02433        return TryGetDictionaryValueIgnoreCase(dict, "Value", out var valueObj)
 02434            ? UnwrapPsObject(valueObj)
 02435            : null;
 2436    }
 2437
 2438    /// <summary>
 2439    /// Unwraps a PowerShell <see cref="PSObject"/> by returning its <see cref="PSObject.BaseObject"/>.
 2440    /// </summary>
 2441    /// <param name="raw">The value to unwrap.</param>
 2442    /// <returns>The underlying base object when <paramref name="raw"/> is a <see cref="PSObject"/>, otherwise <paramref
 2443    private static object? UnwrapPsObject(object? raw)
 32444        => raw is PSObject pso ? pso.BaseObject : raw;
 2445
 2446    /// <summary>
 2447    /// Determines whether the Kestrun variable marker is enabled.
 2448    /// </summary>
 2449    /// <param name="markerObj">The marker value (typically a boolean or a PowerShell-wrapped boolean).</param>
 2450    /// <returns><c>true</c> if the marker indicates the value is wrapped; otherwise, <c>false</c>.</returns>
 2451    private static bool IsKestrunVariableMarkerEnabled(object? markerObj)
 02452        => markerObj switch
 02453        {
 02454            bool b => b,
 02455            PSObject psMarker when psMarker.BaseObject is bool b => b,
 02456            _ => false
 02457        };
 2458
 2459    private static bool TryGetDictionaryValueIgnoreCase(System.Collections.IDictionary dict, string key, out object? val
 2460    {
 02461        value = null;
 2462
 02463        if (dict.Contains(key))
 2464        {
 02465            value = dict[key];
 02466            return true;
 2467        }
 2468
 02469        foreach (System.Collections.DictionaryEntry de in dict)
 2470        {
 02471            if (de.Key is string s && string.Equals(s, key, StringComparison.OrdinalIgnoreCase))
 2472            {
 02473                value = de.Value;
 02474                return true;
 2475            }
 2476        }
 2477
 02478        return false;
 02479    }
 2480
 2481    private static void AddUserFunctions(InitialSessionState iss, IReadOnlyDictionary<string, string>? userFunctions)
 2482    {
 692483        if (userFunctions is null)
 2484        {
 662485            return;
 2486        }
 2487
 122488        foreach (var function in userFunctions)
 2489        {
 32490            var entry = new SessionStateFunctionEntry(
 32491                function.Key,
 32492                function.Value,
 32493                ScopedItemOptions.ReadOnly,
 32494                helpFile: null
 32495            );
 2496
 32497            iss.Commands.Add(entry);
 2498        }
 32499    }
 2500
 2501    private static int ResolveMaxRunspaces(int? maxRunspaces) =>
 692502        (maxRunspaces.HasValue && maxRunspaces.Value > 0)
 692503            ? maxRunspaces.Value
 692504            : Environment.ProcessorCount * 2;
 2505
 2506    #endregion
 2507
 2508    #region Disposable
 2509
 2510    /// <summary>
 2511    /// Releases all resources used by the <see cref="KestrunHost"/> instance.
 2512    /// </summary>
 2513    public void Dispose()
 2514    {
 2742515        if (Logger.IsEnabled(LogEventLevel.Debug))
 2516        {
 2672517            Logger.Debug("Dispose() called");
 2518        }
 2519
 2742520        _runspacePool?.Dispose();
 2742521        _runspacePool = null; // Clear the runspace pool reference
 2742522        IsConfigured = false; // Reset configuration state
 2742523        _app = null;
 2742524        _scheduler?.Dispose();
 2742525        (Logger as IDisposable)?.Dispose();
 2742526        GC.SuppressFinalize(this);
 2742527    }
 2528    #endregion
 2529
 2530    #region Script Validation
 2531
 2532    #endregion
 2533}

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_Runtime()
get_ApplicationName()
get_Options()
.ctor(System.String,Serilog.ILogger,System.String,System.String[],System.String[],System.Boolean)
get_IsConfigured()
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_PowerShellErrorResponseScript()
set_PowerShellErrorResponseScript(System.String)
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()
AddFormOption(Kestrun.Forms.KrFormOptions)
GetFormOption(System.String)
AddFormPartRule(Kestrun.Forms.KrFormPartRule)
GetFormPartRule(System.String)
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>)
NormalizeHttp3ListenersAndConfigureQuic()
IsQuicSupported()
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()
PopulateAppUrlsFromListeners()
ResolveEphemeralPort(System.Net.IPAddress)
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()