diff --git a/Hosuto.sln b/Hosuto.sln index 7373561..3d0b828 100644 --- a/Hosuto.sln +++ b/Hosuto.sln @@ -49,76 +49,240 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "App", "samples\dotnet\App\A EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorLibrary", "samples\dotnet\RazorLibrary\RazorLibrary.csproj", "{71E30ED2-53D6-4A9D-B541-4255D88E97AD}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "minimal", "minimal", "{8CBA57D1-6EFE-D619-F938-253FEAFA8EE9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "samples\dotnet\minimal\App\App.csproj", "{EB49E8F1-D31A-46E5-98BE-F525FFF44DBC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RazorModule", "samples\dotnet\minimal\RazorModule\RazorModule.csproj", "{961CB672-6FAB-4F8A-90F4-5EB074ADEE97}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {1B1C938C-B367-4EF7-8AE2-7832B857DB7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1B1C938C-B367-4EF7-8AE2-7832B857DB7B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B1C938C-B367-4EF7-8AE2-7832B857DB7B}.Debug|x64.ActiveCfg = Debug|Any CPU + {1B1C938C-B367-4EF7-8AE2-7832B857DB7B}.Debug|x64.Build.0 = Debug|Any CPU + {1B1C938C-B367-4EF7-8AE2-7832B857DB7B}.Debug|x86.ActiveCfg = Debug|Any CPU + {1B1C938C-B367-4EF7-8AE2-7832B857DB7B}.Debug|x86.Build.0 = Debug|Any CPU {1B1C938C-B367-4EF7-8AE2-7832B857DB7B}.Release|Any CPU.ActiveCfg = Release|Any CPU {1B1C938C-B367-4EF7-8AE2-7832B857DB7B}.Release|Any CPU.Build.0 = Release|Any CPU + {1B1C938C-B367-4EF7-8AE2-7832B857DB7B}.Release|x64.ActiveCfg = Release|Any CPU + {1B1C938C-B367-4EF7-8AE2-7832B857DB7B}.Release|x64.Build.0 = Release|Any CPU + {1B1C938C-B367-4EF7-8AE2-7832B857DB7B}.Release|x86.ActiveCfg = Release|Any CPU + {1B1C938C-B367-4EF7-8AE2-7832B857DB7B}.Release|x86.Build.0 = Release|Any CPU {D78ABD82-026C-4393-ABAB-E9CF551DF7E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D78ABD82-026C-4393-ABAB-E9CF551DF7E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D78ABD82-026C-4393-ABAB-E9CF551DF7E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {D78ABD82-026C-4393-ABAB-E9CF551DF7E1}.Debug|x64.Build.0 = Debug|Any CPU + {D78ABD82-026C-4393-ABAB-E9CF551DF7E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {D78ABD82-026C-4393-ABAB-E9CF551DF7E1}.Debug|x86.Build.0 = Debug|Any CPU {D78ABD82-026C-4393-ABAB-E9CF551DF7E1}.Release|Any CPU.ActiveCfg = Release|Any CPU {D78ABD82-026C-4393-ABAB-E9CF551DF7E1}.Release|Any CPU.Build.0 = Release|Any CPU + {D78ABD82-026C-4393-ABAB-E9CF551DF7E1}.Release|x64.ActiveCfg = Release|Any CPU + {D78ABD82-026C-4393-ABAB-E9CF551DF7E1}.Release|x64.Build.0 = Release|Any CPU + {D78ABD82-026C-4393-ABAB-E9CF551DF7E1}.Release|x86.ActiveCfg = Release|Any CPU + {D78ABD82-026C-4393-ABAB-E9CF551DF7E1}.Release|x86.Build.0 = Release|Any CPU {26C793F8-EF85-4C4B-AD70-096D0758631D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {26C793F8-EF85-4C4B-AD70-096D0758631D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26C793F8-EF85-4C4B-AD70-096D0758631D}.Debug|x64.ActiveCfg = Debug|Any CPU + {26C793F8-EF85-4C4B-AD70-096D0758631D}.Debug|x64.Build.0 = Debug|Any CPU + {26C793F8-EF85-4C4B-AD70-096D0758631D}.Debug|x86.ActiveCfg = Debug|Any CPU + {26C793F8-EF85-4C4B-AD70-096D0758631D}.Debug|x86.Build.0 = Debug|Any CPU {26C793F8-EF85-4C4B-AD70-096D0758631D}.Release|Any CPU.ActiveCfg = Release|Any CPU {26C793F8-EF85-4C4B-AD70-096D0758631D}.Release|Any CPU.Build.0 = Release|Any CPU + {26C793F8-EF85-4C4B-AD70-096D0758631D}.Release|x64.ActiveCfg = Release|Any CPU + {26C793F8-EF85-4C4B-AD70-096D0758631D}.Release|x64.Build.0 = Release|Any CPU + {26C793F8-EF85-4C4B-AD70-096D0758631D}.Release|x86.ActiveCfg = Release|Any CPU + {26C793F8-EF85-4C4B-AD70-096D0758631D}.Release|x86.Build.0 = Release|Any CPU {0D000FD6-F2EE-4658-9DB0-A28CF977CA09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0D000FD6-F2EE-4658-9DB0-A28CF977CA09}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D000FD6-F2EE-4658-9DB0-A28CF977CA09}.Debug|x64.ActiveCfg = Debug|Any CPU + {0D000FD6-F2EE-4658-9DB0-A28CF977CA09}.Debug|x64.Build.0 = Debug|Any CPU + {0D000FD6-F2EE-4658-9DB0-A28CF977CA09}.Debug|x86.ActiveCfg = Debug|Any CPU + {0D000FD6-F2EE-4658-9DB0-A28CF977CA09}.Debug|x86.Build.0 = Debug|Any CPU {0D000FD6-F2EE-4658-9DB0-A28CF977CA09}.Release|Any CPU.ActiveCfg = Release|Any CPU {0D000FD6-F2EE-4658-9DB0-A28CF977CA09}.Release|Any CPU.Build.0 = Release|Any CPU + {0D000FD6-F2EE-4658-9DB0-A28CF977CA09}.Release|x64.ActiveCfg = Release|Any CPU + {0D000FD6-F2EE-4658-9DB0-A28CF977CA09}.Release|x64.Build.0 = Release|Any CPU + {0D000FD6-F2EE-4658-9DB0-A28CF977CA09}.Release|x86.ActiveCfg = Release|Any CPU + {0D000FD6-F2EE-4658-9DB0-A28CF977CA09}.Release|x86.Build.0 = Release|Any CPU {B0DF33E2-2F24-4C0F-9F5A-9EFD1D714FC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B0DF33E2-2F24-4C0F-9F5A-9EFD1D714FC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0DF33E2-2F24-4C0F-9F5A-9EFD1D714FC4}.Debug|x64.ActiveCfg = Debug|Any CPU + {B0DF33E2-2F24-4C0F-9F5A-9EFD1D714FC4}.Debug|x64.Build.0 = Debug|Any CPU + {B0DF33E2-2F24-4C0F-9F5A-9EFD1D714FC4}.Debug|x86.ActiveCfg = Debug|Any CPU + {B0DF33E2-2F24-4C0F-9F5A-9EFD1D714FC4}.Debug|x86.Build.0 = Debug|Any CPU {B0DF33E2-2F24-4C0F-9F5A-9EFD1D714FC4}.Release|Any CPU.ActiveCfg = Release|Any CPU {B0DF33E2-2F24-4C0F-9F5A-9EFD1D714FC4}.Release|Any CPU.Build.0 = Release|Any CPU + {B0DF33E2-2F24-4C0F-9F5A-9EFD1D714FC4}.Release|x64.ActiveCfg = Release|Any CPU + {B0DF33E2-2F24-4C0F-9F5A-9EFD1D714FC4}.Release|x64.Build.0 = Release|Any CPU + {B0DF33E2-2F24-4C0F-9F5A-9EFD1D714FC4}.Release|x86.ActiveCfg = Release|Any CPU + {B0DF33E2-2F24-4C0F-9F5A-9EFD1D714FC4}.Release|x86.Build.0 = Release|Any CPU {29C629D3-033F-4112-9F31-F0285D91FC71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {29C629D3-033F-4112-9F31-F0285D91FC71}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29C629D3-033F-4112-9F31-F0285D91FC71}.Debug|x64.ActiveCfg = Debug|Any CPU + {29C629D3-033F-4112-9F31-F0285D91FC71}.Debug|x64.Build.0 = Debug|Any CPU + {29C629D3-033F-4112-9F31-F0285D91FC71}.Debug|x86.ActiveCfg = Debug|Any CPU + {29C629D3-033F-4112-9F31-F0285D91FC71}.Debug|x86.Build.0 = Debug|Any CPU {29C629D3-033F-4112-9F31-F0285D91FC71}.Release|Any CPU.ActiveCfg = Release|Any CPU {29C629D3-033F-4112-9F31-F0285D91FC71}.Release|Any CPU.Build.0 = Release|Any CPU + {29C629D3-033F-4112-9F31-F0285D91FC71}.Release|x64.ActiveCfg = Release|Any CPU + {29C629D3-033F-4112-9F31-F0285D91FC71}.Release|x64.Build.0 = Release|Any CPU + {29C629D3-033F-4112-9F31-F0285D91FC71}.Release|x86.ActiveCfg = Release|Any CPU + {29C629D3-033F-4112-9F31-F0285D91FC71}.Release|x86.Build.0 = Release|Any CPU {ECECF074-B3EF-428C-B3F6-4975200F7E1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ECECF074-B3EF-428C-B3F6-4975200F7E1A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ECECF074-B3EF-428C-B3F6-4975200F7E1A}.Debug|x64.ActiveCfg = Debug|Any CPU + {ECECF074-B3EF-428C-B3F6-4975200F7E1A}.Debug|x64.Build.0 = Debug|Any CPU + {ECECF074-B3EF-428C-B3F6-4975200F7E1A}.Debug|x86.ActiveCfg = Debug|Any CPU + {ECECF074-B3EF-428C-B3F6-4975200F7E1A}.Debug|x86.Build.0 = Debug|Any CPU {ECECF074-B3EF-428C-B3F6-4975200F7E1A}.Release|Any CPU.ActiveCfg = Release|Any CPU {ECECF074-B3EF-428C-B3F6-4975200F7E1A}.Release|Any CPU.Build.0 = Release|Any CPU + {ECECF074-B3EF-428C-B3F6-4975200F7E1A}.Release|x64.ActiveCfg = Release|Any CPU + {ECECF074-B3EF-428C-B3F6-4975200F7E1A}.Release|x64.Build.0 = Release|Any CPU + {ECECF074-B3EF-428C-B3F6-4975200F7E1A}.Release|x86.ActiveCfg = Release|Any CPU + {ECECF074-B3EF-428C-B3F6-4975200F7E1A}.Release|x86.Build.0 = Release|Any CPU {17D33BF9-3D31-47D1-8561-D3194A5D18C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {17D33BF9-3D31-47D1-8561-D3194A5D18C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17D33BF9-3D31-47D1-8561-D3194A5D18C5}.Debug|x64.ActiveCfg = Debug|Any CPU + {17D33BF9-3D31-47D1-8561-D3194A5D18C5}.Debug|x64.Build.0 = Debug|Any CPU + {17D33BF9-3D31-47D1-8561-D3194A5D18C5}.Debug|x86.ActiveCfg = Debug|Any CPU + {17D33BF9-3D31-47D1-8561-D3194A5D18C5}.Debug|x86.Build.0 = Debug|Any CPU {17D33BF9-3D31-47D1-8561-D3194A5D18C5}.Release|Any CPU.ActiveCfg = Release|Any CPU {17D33BF9-3D31-47D1-8561-D3194A5D18C5}.Release|Any CPU.Build.0 = Release|Any CPU + {17D33BF9-3D31-47D1-8561-D3194A5D18C5}.Release|x64.ActiveCfg = Release|Any CPU + {17D33BF9-3D31-47D1-8561-D3194A5D18C5}.Release|x64.Build.0 = Release|Any CPU + {17D33BF9-3D31-47D1-8561-D3194A5D18C5}.Release|x86.ActiveCfg = Release|Any CPU + {17D33BF9-3D31-47D1-8561-D3194A5D18C5}.Release|x86.Build.0 = Release|Any CPU {D4A5C193-51F1-4C10-BE03-C940090F245F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D4A5C193-51F1-4C10-BE03-C940090F245F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4A5C193-51F1-4C10-BE03-C940090F245F}.Debug|x64.ActiveCfg = Debug|Any CPU + {D4A5C193-51F1-4C10-BE03-C940090F245F}.Debug|x64.Build.0 = Debug|Any CPU + {D4A5C193-51F1-4C10-BE03-C940090F245F}.Debug|x86.ActiveCfg = Debug|Any CPU + {D4A5C193-51F1-4C10-BE03-C940090F245F}.Debug|x86.Build.0 = Debug|Any CPU {D4A5C193-51F1-4C10-BE03-C940090F245F}.Release|Any CPU.ActiveCfg = Release|Any CPU {D4A5C193-51F1-4C10-BE03-C940090F245F}.Release|Any CPU.Build.0 = Release|Any CPU + {D4A5C193-51F1-4C10-BE03-C940090F245F}.Release|x64.ActiveCfg = Release|Any CPU + {D4A5C193-51F1-4C10-BE03-C940090F245F}.Release|x64.Build.0 = Release|Any CPU + {D4A5C193-51F1-4C10-BE03-C940090F245F}.Release|x86.ActiveCfg = Release|Any CPU + {D4A5C193-51F1-4C10-BE03-C940090F245F}.Release|x86.Build.0 = Release|Any CPU {4F525B97-E934-4F9B-AB40-78CBF6B61269}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4F525B97-E934-4F9B-AB40-78CBF6B61269}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F525B97-E934-4F9B-AB40-78CBF6B61269}.Debug|x64.ActiveCfg = Debug|Any CPU + {4F525B97-E934-4F9B-AB40-78CBF6B61269}.Debug|x64.Build.0 = Debug|Any CPU + {4F525B97-E934-4F9B-AB40-78CBF6B61269}.Debug|x86.ActiveCfg = Debug|Any CPU + {4F525B97-E934-4F9B-AB40-78CBF6B61269}.Debug|x86.Build.0 = Debug|Any CPU {4F525B97-E934-4F9B-AB40-78CBF6B61269}.Release|Any CPU.ActiveCfg = Release|Any CPU {4F525B97-E934-4F9B-AB40-78CBF6B61269}.Release|Any CPU.Build.0 = Release|Any CPU + {4F525B97-E934-4F9B-AB40-78CBF6B61269}.Release|x64.ActiveCfg = Release|Any CPU + {4F525B97-E934-4F9B-AB40-78CBF6B61269}.Release|x64.Build.0 = Release|Any CPU + {4F525B97-E934-4F9B-AB40-78CBF6B61269}.Release|x86.ActiveCfg = Release|Any CPU + {4F525B97-E934-4F9B-AB40-78CBF6B61269}.Release|x86.Build.0 = Release|Any CPU {5101C6BF-94D4-4001-AA0B-AC863E6E79F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5101C6BF-94D4-4001-AA0B-AC863E6E79F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5101C6BF-94D4-4001-AA0B-AC863E6E79F2}.Debug|x64.ActiveCfg = Debug|Any CPU + {5101C6BF-94D4-4001-AA0B-AC863E6E79F2}.Debug|x64.Build.0 = Debug|Any CPU + {5101C6BF-94D4-4001-AA0B-AC863E6E79F2}.Debug|x86.ActiveCfg = Debug|Any CPU + {5101C6BF-94D4-4001-AA0B-AC863E6E79F2}.Debug|x86.Build.0 = Debug|Any CPU {5101C6BF-94D4-4001-AA0B-AC863E6E79F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {5101C6BF-94D4-4001-AA0B-AC863E6E79F2}.Release|Any CPU.Build.0 = Release|Any CPU + {5101C6BF-94D4-4001-AA0B-AC863E6E79F2}.Release|x64.ActiveCfg = Release|Any CPU + {5101C6BF-94D4-4001-AA0B-AC863E6E79F2}.Release|x64.Build.0 = Release|Any CPU + {5101C6BF-94D4-4001-AA0B-AC863E6E79F2}.Release|x86.ActiveCfg = Release|Any CPU + {5101C6BF-94D4-4001-AA0B-AC863E6E79F2}.Release|x86.Build.0 = Release|Any CPU {C650ED86-171D-4823-B334-F696FED6C471}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C650ED86-171D-4823-B334-F696FED6C471}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C650ED86-171D-4823-B334-F696FED6C471}.Debug|x64.ActiveCfg = Debug|Any CPU + {C650ED86-171D-4823-B334-F696FED6C471}.Debug|x64.Build.0 = Debug|Any CPU + {C650ED86-171D-4823-B334-F696FED6C471}.Debug|x86.ActiveCfg = Debug|Any CPU + {C650ED86-171D-4823-B334-F696FED6C471}.Debug|x86.Build.0 = Debug|Any CPU {C650ED86-171D-4823-B334-F696FED6C471}.Release|Any CPU.ActiveCfg = Release|Any CPU {C650ED86-171D-4823-B334-F696FED6C471}.Release|Any CPU.Build.0 = Release|Any CPU + {C650ED86-171D-4823-B334-F696FED6C471}.Release|x64.ActiveCfg = Release|Any CPU + {C650ED86-171D-4823-B334-F696FED6C471}.Release|x64.Build.0 = Release|Any CPU + {C650ED86-171D-4823-B334-F696FED6C471}.Release|x86.ActiveCfg = Release|Any CPU + {C650ED86-171D-4823-B334-F696FED6C471}.Release|x86.Build.0 = Release|Any CPU {B16EA0A4-8EBE-4EAC-ACE9-538572951747}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B16EA0A4-8EBE-4EAC-ACE9-538572951747}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B16EA0A4-8EBE-4EAC-ACE9-538572951747}.Debug|x64.ActiveCfg = Debug|Any CPU + {B16EA0A4-8EBE-4EAC-ACE9-538572951747}.Debug|x64.Build.0 = Debug|Any CPU + {B16EA0A4-8EBE-4EAC-ACE9-538572951747}.Debug|x86.ActiveCfg = Debug|Any CPU + {B16EA0A4-8EBE-4EAC-ACE9-538572951747}.Debug|x86.Build.0 = Debug|Any CPU {B16EA0A4-8EBE-4EAC-ACE9-538572951747}.Release|Any CPU.ActiveCfg = Release|Any CPU {B16EA0A4-8EBE-4EAC-ACE9-538572951747}.Release|Any CPU.Build.0 = Release|Any CPU + {B16EA0A4-8EBE-4EAC-ACE9-538572951747}.Release|x64.ActiveCfg = Release|Any CPU + {B16EA0A4-8EBE-4EAC-ACE9-538572951747}.Release|x64.Build.0 = Release|Any CPU + {B16EA0A4-8EBE-4EAC-ACE9-538572951747}.Release|x86.ActiveCfg = Release|Any CPU + {B16EA0A4-8EBE-4EAC-ACE9-538572951747}.Release|x86.Build.0 = Release|Any CPU {1C6AD780-6DC3-4AF1-AB4E-51ABC1FC3A75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1C6AD780-6DC3-4AF1-AB4E-51ABC1FC3A75}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C6AD780-6DC3-4AF1-AB4E-51ABC1FC3A75}.Debug|x64.ActiveCfg = Debug|Any CPU + {1C6AD780-6DC3-4AF1-AB4E-51ABC1FC3A75}.Debug|x64.Build.0 = Debug|Any CPU + {1C6AD780-6DC3-4AF1-AB4E-51ABC1FC3A75}.Debug|x86.ActiveCfg = Debug|Any CPU + {1C6AD780-6DC3-4AF1-AB4E-51ABC1FC3A75}.Debug|x86.Build.0 = Debug|Any CPU {1C6AD780-6DC3-4AF1-AB4E-51ABC1FC3A75}.Release|Any CPU.ActiveCfg = Release|Any CPU {1C6AD780-6DC3-4AF1-AB4E-51ABC1FC3A75}.Release|Any CPU.Build.0 = Release|Any CPU + {1C6AD780-6DC3-4AF1-AB4E-51ABC1FC3A75}.Release|x64.ActiveCfg = Release|Any CPU + {1C6AD780-6DC3-4AF1-AB4E-51ABC1FC3A75}.Release|x64.Build.0 = Release|Any CPU + {1C6AD780-6DC3-4AF1-AB4E-51ABC1FC3A75}.Release|x86.ActiveCfg = Release|Any CPU + {1C6AD780-6DC3-4AF1-AB4E-51ABC1FC3A75}.Release|x86.Build.0 = Release|Any CPU {1BFA0E1B-556C-49DA-81F0-1067FAD97458}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1BFA0E1B-556C-49DA-81F0-1067FAD97458}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1BFA0E1B-556C-49DA-81F0-1067FAD97458}.Debug|x64.ActiveCfg = Debug|Any CPU + {1BFA0E1B-556C-49DA-81F0-1067FAD97458}.Debug|x64.Build.0 = Debug|Any CPU + {1BFA0E1B-556C-49DA-81F0-1067FAD97458}.Debug|x86.ActiveCfg = Debug|Any CPU + {1BFA0E1B-556C-49DA-81F0-1067FAD97458}.Debug|x86.Build.0 = Debug|Any CPU {1BFA0E1B-556C-49DA-81F0-1067FAD97458}.Release|Any CPU.ActiveCfg = Release|Any CPU {1BFA0E1B-556C-49DA-81F0-1067FAD97458}.Release|Any CPU.Build.0 = Release|Any CPU + {1BFA0E1B-556C-49DA-81F0-1067FAD97458}.Release|x64.ActiveCfg = Release|Any CPU + {1BFA0E1B-556C-49DA-81F0-1067FAD97458}.Release|x64.Build.0 = Release|Any CPU + {1BFA0E1B-556C-49DA-81F0-1067FAD97458}.Release|x86.ActiveCfg = Release|Any CPU + {1BFA0E1B-556C-49DA-81F0-1067FAD97458}.Release|x86.Build.0 = Release|Any CPU {71E30ED2-53D6-4A9D-B541-4255D88E97AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {71E30ED2-53D6-4A9D-B541-4255D88E97AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71E30ED2-53D6-4A9D-B541-4255D88E97AD}.Debug|x64.ActiveCfg = Debug|Any CPU + {71E30ED2-53D6-4A9D-B541-4255D88E97AD}.Debug|x64.Build.0 = Debug|Any CPU + {71E30ED2-53D6-4A9D-B541-4255D88E97AD}.Debug|x86.ActiveCfg = Debug|Any CPU + {71E30ED2-53D6-4A9D-B541-4255D88E97AD}.Debug|x86.Build.0 = Debug|Any CPU {71E30ED2-53D6-4A9D-B541-4255D88E97AD}.Release|Any CPU.ActiveCfg = Release|Any CPU {71E30ED2-53D6-4A9D-B541-4255D88E97AD}.Release|Any CPU.Build.0 = Release|Any CPU + {71E30ED2-53D6-4A9D-B541-4255D88E97AD}.Release|x64.ActiveCfg = Release|Any CPU + {71E30ED2-53D6-4A9D-B541-4255D88E97AD}.Release|x64.Build.0 = Release|Any CPU + {71E30ED2-53D6-4A9D-B541-4255D88E97AD}.Release|x86.ActiveCfg = Release|Any CPU + {71E30ED2-53D6-4A9D-B541-4255D88E97AD}.Release|x86.Build.0 = Release|Any CPU + {EB49E8F1-D31A-46E5-98BE-F525FFF44DBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB49E8F1-D31A-46E5-98BE-F525FFF44DBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB49E8F1-D31A-46E5-98BE-F525FFF44DBC}.Debug|x64.ActiveCfg = Debug|Any CPU + {EB49E8F1-D31A-46E5-98BE-F525FFF44DBC}.Debug|x64.Build.0 = Debug|Any CPU + {EB49E8F1-D31A-46E5-98BE-F525FFF44DBC}.Debug|x86.ActiveCfg = Debug|Any CPU + {EB49E8F1-D31A-46E5-98BE-F525FFF44DBC}.Debug|x86.Build.0 = Debug|Any CPU + {EB49E8F1-D31A-46E5-98BE-F525FFF44DBC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB49E8F1-D31A-46E5-98BE-F525FFF44DBC}.Release|Any CPU.Build.0 = Release|Any CPU + {EB49E8F1-D31A-46E5-98BE-F525FFF44DBC}.Release|x64.ActiveCfg = Release|Any CPU + {EB49E8F1-D31A-46E5-98BE-F525FFF44DBC}.Release|x64.Build.0 = Release|Any CPU + {EB49E8F1-D31A-46E5-98BE-F525FFF44DBC}.Release|x86.ActiveCfg = Release|Any CPU + {EB49E8F1-D31A-46E5-98BE-F525FFF44DBC}.Release|x86.Build.0 = Release|Any CPU + {961CB672-6FAB-4F8A-90F4-5EB074ADEE97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {961CB672-6FAB-4F8A-90F4-5EB074ADEE97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {961CB672-6FAB-4F8A-90F4-5EB074ADEE97}.Debug|x64.ActiveCfg = Debug|Any CPU + {961CB672-6FAB-4F8A-90F4-5EB074ADEE97}.Debug|x64.Build.0 = Debug|Any CPU + {961CB672-6FAB-4F8A-90F4-5EB074ADEE97}.Debug|x86.ActiveCfg = Debug|Any CPU + {961CB672-6FAB-4F8A-90F4-5EB074ADEE97}.Debug|x86.Build.0 = Debug|Any CPU + {961CB672-6FAB-4F8A-90F4-5EB074ADEE97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {961CB672-6FAB-4F8A-90F4-5EB074ADEE97}.Release|Any CPU.Build.0 = Release|Any CPU + {961CB672-6FAB-4F8A-90F4-5EB074ADEE97}.Release|x64.ActiveCfg = Release|Any CPU + {961CB672-6FAB-4F8A-90F4-5EB074ADEE97}.Release|x64.Build.0 = Release|Any CPU + {961CB672-6FAB-4F8A-90F4-5EB074ADEE97}.Release|x86.ActiveCfg = Release|Any CPU + {961CB672-6FAB-4F8A-90F4-5EB074ADEE97}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -134,6 +298,9 @@ Global {1C6AD780-6DC3-4AF1-AB4E-51ABC1FC3A75} = {F03A55C6-AB61-4A07-8E06-A742A1E9F625} {1BFA0E1B-556C-49DA-81F0-1067FAD97458} = {F03A55C6-AB61-4A07-8E06-A742A1E9F625} {71E30ED2-53D6-4A9D-B541-4255D88E97AD} = {F03A55C6-AB61-4A07-8E06-A742A1E9F625} + {8CBA57D1-6EFE-D619-F938-253FEAFA8EE9} = {F03A55C6-AB61-4A07-8E06-A742A1E9F625} + {EB49E8F1-D31A-46E5-98BE-F525FFF44DBC} = {8CBA57D1-6EFE-D619-F938-253FEAFA8EE9} + {961CB672-6FAB-4F8A-90F4-5EB074ADEE97} = {8CBA57D1-6EFE-D619-F938-253FEAFA8EE9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C7C4A16F-35F6-40CB-9F59-B48D0757FA7C} diff --git a/samples/dotnet/minimal/App/App.csproj b/samples/dotnet/minimal/App/App.csproj new file mode 100644 index 0000000..0e08d3f --- /dev/null +++ b/samples/dotnet/minimal/App/App.csproj @@ -0,0 +1,16 @@ + + + + Exe + net10.0 + false + disable + disable + Dbosoft.Hosuto.Samples.Minimal.App + + + + + + + diff --git a/samples/dotnet/minimal/App/Program.cs b/samples/dotnet/minimal/App/Program.cs new file mode 100644 index 0000000..2063229 --- /dev/null +++ b/samples/dotnet/minimal/App/Program.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Dbosoft.Hosuto.Modules.Hosting; +using Dbosoft.Hosuto.Samples.Minimal.RazorModule; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.Extensions.DependencyInjection; +using SimpleInjector; + +namespace Dbosoft.Hosuto.Samples.Minimal.App +{ + public static class Program + { + public static async Task Main() + { + var builder = ModulesHost.CreateDefaultBuilder(); + builder.UseSimpleInjector(new Container()); + builder.UseAspNetCoreMinimal(app => app.WebHost.UseUrls("http://127.0.0.1:0")); + builder.HostModule(); + + using var host = builder.Build(); + await host.StartAsync(); + + var moduleHost = host.Services.GetRequiredService>(); + var baseUrl = moduleHost.Services.GetRequiredService() + .Features.Get().Addresses.First(); + + using (var http = new HttpClient()) + { + await Probe(http, baseUrl + "/", "Razor page"); + await Probe(http, baseUrl + "/css/site.css", "static asset (wwwroot)"); + } + + await host.StopAsync(); + } + + private static async Task Probe(HttpClient http, string url, string what) + { + try + { + var response = await http.GetAsync(url); + var body = await response.Content.ReadAsStringAsync(); + var snippet = body.Replace("\r", " ").Replace("\n", " ").Trim(); + if (snippet.Length > 70) snippet = snippet.Substring(0, 70) + "..."; + Console.WriteLine($"[{what,-22}] {(int)response.StatusCode} {response.StatusCode} {url}"); + Console.WriteLine($" -> {snippet}"); + } + catch (Exception ex) + { + Console.WriteLine($"[{what,-22}] ERROR {url} -> {ex.Message}"); + } + } + } +} diff --git a/samples/dotnet/minimal/README.md b/samples/dotnet/minimal/README.md new file mode 100644 index 0000000..344d5b5 --- /dev/null +++ b/samples/dotnet/minimal/README.md @@ -0,0 +1,33 @@ +# Minimal-API host sample (net10, Razor) + +Demonstrates the opt-in **`UseAspNetCoreMinimal()`** path: a Hosuto web module is hosted on a +minimal-API `WebApplication` inner host instead of the classic `ConfigureWebHostDefaults` host. The +module (`RazorModule`) is an ordinary Hosuto `WebModule` using the module interfaces +(`IServiceConfiguringModule` / `IApplicationConfiguringModule`) and is authored exactly like a +module for the multi-host builder. + +Run it: + +``` +dotnet run --project App +``` + +The host starts the module on an ephemeral port, then probes two URLs and prints the results. + +## What works + +- `GET /` → **200** — Razor Pages are discovered and rendered on the minimal-API module host. +- Dependency injection / SimpleInjector module container (`ConfigureContainer`) works post-build. + +## Known limitation (tracked as a follow-up) + +- `GET /css/site.css` → **404** — a module's **static web assets are not yet mapped** on the + minimal-API host. + + The classic host wires module assets via `ModuleWebAssetsLoader`, which is **file-provider / + XML-manifest** based (`{app}.StaticWebAssets.xml`) and manipulates `WebRootFileProvider`. Since + .NET 9 static web assets are **endpoint-based** (`MapStaticAssets`, + `{app}.staticwebassets.*.json`), that mechanism no longer applies. Mapping a module's static web + assets on net9/10 (filtering the host manifest's `.modules/{module}` entries into the module host) + is a separate piece of work and is intentionally **not** part of the initial `UseAspNetCoreMinimal` + handler. diff --git a/samples/dotnet/minimal/RazorModule/Pages/Index.cshtml b/samples/dotnet/minimal/RazorModule/Pages/Index.cshtml new file mode 100644 index 0000000..6f47d09 --- /dev/null +++ b/samples/dotnet/minimal/RazorModule/Pages/Index.cshtml @@ -0,0 +1,14 @@ +@page +@{ + Layout = null; +} + + + + Razor module + + + +

Razor module page rendered

+ + diff --git a/samples/dotnet/minimal/RazorModule/RazorGreetModule.cs b/samples/dotnet/minimal/RazorModule/RazorGreetModule.cs new file mode 100644 index 0000000..449f043 --- /dev/null +++ b/samples/dotnet/minimal/RazorModule/RazorGreetModule.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Dbosoft.Hosuto.Modules; + +namespace Dbosoft.Hosuto.Samples.Minimal.RazorModule +{ + // A Razor web module on the minimal-API WebApplication inner host, authored with the three + // module contracts - services, middleware, and (minimal-API-style) endpoints: + // IServiceConfiguringModule -> AddRazorPages + // IApplicationConfiguringModule -> UseStaticFiles (middleware) + // IEndpointConfiguringModule -> MapRazorPages (idiomatic endpoint mapping) + public sealed class RazorGreetModule + : WebModule, IServiceConfiguringModule, IApplicationConfiguringModule, IEndpointConfiguringModule + { + public override string Path { get; } = ""; + + public void ConfigureServices(IServiceProvider serviceProvider, IServiceCollection services) + { + services.AddRazorPages(); + } + + public void Configure(IServiceProvider serviceProvider, IApplicationBuilder app) + { + app.UseStaticFiles(); + } + + public void MapEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder endpoints) + { + endpoints.MapRazorPages(); + } + } +} diff --git a/samples/dotnet/minimal/RazorModule/RazorModule.csproj b/samples/dotnet/minimal/RazorModule/RazorModule.csproj new file mode 100644 index 0000000..154721b --- /dev/null +++ b/samples/dotnet/minimal/RazorModule/RazorModule.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + Library + true + false + disable + disable + Dbosoft.Hosuto.Samples.Minimal.RazorModule + Dbosoft.Hosuto.Samples.Minimal.RazorModule + + .modules/$(AssemblyName) + + + + + + + + + + + + diff --git a/samples/dotnet/minimal/RazorModule/wwwroot/css/site.css b/samples/dotnet/minimal/RazorModule/wwwroot/css/site.css new file mode 100644 index 0000000..f2a1506 --- /dev/null +++ b/samples/dotnet/minimal/RazorModule/wwwroot/css/site.css @@ -0,0 +1 @@ +#msg { color: green; } diff --git a/src/Hosuto.Hosting.AspNetCore/Modules/Hosting/ModuleConfigureStartupFilter.cs b/src/Hosuto.Hosting.AspNetCore/Modules/Hosting/ModuleConfigureStartupFilter.cs new file mode 100644 index 0000000..fdcdd28 --- /dev/null +++ b/src/Hosuto.Hosting.AspNetCore/Modules/Hosting/ModuleConfigureStartupFilter.cs @@ -0,0 +1,50 @@ +#if NET6_0_OR_GREATER +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; + +namespace Dbosoft.Hosuto.Modules.Hosting +{ + // Runs the module's Configure delegate when the request pipeline is built (at start), which is + // after the module container has been configured by the bootstrap pipeline. + internal sealed class ModuleConfigureStartupFilter : IStartupFilter + { + private readonly Action _configure; + + public ModuleConfigureStartupFilter(Action configure) + { + _configure = configure; + } + + public Action Configure(Action next) + { + return app => + { + _configure(app); + next(app); + }; + } + } + + /// + /// Opt-in hook to configure the minimal-API of a web module + /// hosted via UseAspNetCoreMinimal(). + /// + public interface IWebApplicationBuilderConfigurer + { + void Configure(WebApplicationBuilder builder); + } + + internal sealed class DelegateWebApplicationBuilderConfigurer : IWebApplicationBuilderConfigurer + { + private readonly Action _configure; + + public DelegateWebApplicationBuilderConfigurer(Action configure) + { + _configure = configure; + } + + public void Configure(WebApplicationBuilder builder) => _configure(builder); + } +} +#endif diff --git a/src/Hosuto.Hosting.AspNetCore/Modules/Hosting/ModulesHostBuilderExtensions.cs b/src/Hosuto.Hosting.AspNetCore/Modules/Hosting/ModulesHostBuilderExtensions.cs index c52657f..047a1f6 100644 --- a/src/Hosuto.Hosting.AspNetCore/Modules/Hosting/ModulesHostBuilderExtensions.cs +++ b/src/Hosuto.Hosting.AspNetCore/Modules/Hosting/ModulesHostBuilderExtensions.cs @@ -37,6 +37,29 @@ public static IModulesHostBuilder UseAspNetCoreWithDefaults(this IModulesHostBui return UseAspNetCore(builder, configure); } +#if NET6_0_OR_GREATER + /// + /// Opt-in: host web modules on a minimal-API + /// inner host instead of the classic ConfigureWebHostDefaults host. Additive - the + /// module authoring model (ConfigureServices/Configure/ConfigureContainer, convention or the + /// module interfaces) is unchanged. + /// + public static IModulesHostBuilder UseAspNetCoreMinimal(this IModulesHostBuilder builder, + Action configure = null) + { + builder.ConfigureFrameworkServices((ctx, services) => + { + services.AddTransient(typeof(IBootstrapHostFilter<>), typeof(WebApplicationBootstrapHostFilter<>)); + + if (configure != null) + services.AddTransient(sp => + new DelegateWebApplicationBuilderConfigurer(configure)); + }); + + return builder; + } +#endif + public static IModulesHostBuilder UseAspNetCore(this IModulesHostBuilder builder, Action configure = null) { diff --git a/src/Hosuto.Hosting.AspNetCore/Modules/Hosting/WebApplicationBootstrapHostFilter.cs b/src/Hosuto.Hosting.AspNetCore/Modules/Hosting/WebApplicationBootstrapHostFilter.cs new file mode 100644 index 0000000..7bc4316 --- /dev/null +++ b/src/Hosuto.Hosting.AspNetCore/Modules/Hosting/WebApplicationBootstrapHostFilter.cs @@ -0,0 +1,19 @@ +#if NET6_0_OR_GREATER +using System; + +namespace Dbosoft.Hosuto.Modules.Hosting +{ + public class WebApplicationBootstrapHostFilter : IBootstrapHostFilter where TModule : class + { + public Action> Invoke(Action> next) + { + return command => + { + var handler = new WebApplicationModuleHostHandler(); + handler.BootstrapHost(command); + next(command); + }; + } + } +} +#endif diff --git a/src/Hosuto.Hosting.AspNetCore/Modules/Hosting/WebApplicationModuleHostHandler.cs b/src/Hosuto.Hosting.AspNetCore/Modules/Hosting/WebApplicationModuleHostHandler.cs new file mode 100644 index 0000000..089a381 --- /dev/null +++ b/src/Hosuto.Hosting.AspNetCore/Modules/Hosting/WebApplicationModuleHostHandler.cs @@ -0,0 +1,134 @@ +#if NET6_0_OR_GREATER +using System.Collections.Generic; +using Dbosoft.Hosuto.Modules.Hosting.Internal; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Dbosoft.Hosuto.Modules.Hosting +{ + /// + /// Builds a web module's inner host as a minimal-API instead of the + /// classic HostBuilder + ConfigureWebHostDefaults host. Opt-in via + /// UseAspNetCoreMinimal(). The module's ConfigureServices runs against + /// before build; Configure is deferred to + /// pipeline build (start) via an so it runs after the module + /// container (e.g. SimpleInjector) has been configured by the bootstrap pipeline. + /// + /// + /// A module hosting options Configure(Action<IHostBuilder>) delegate is applied to + /// , which restricts some + /// operations (it manages the service provider factory itself). + /// + public class WebApplicationModuleHostHandler : DefaultBootstrapHostHandler where TModule : class + { + public override void BootstrapHost(BootstrapModuleHostCommand command) + { + if (command.Host != null || !(command.BootstrapContext.Module is IWebModule)) + return; + + BootstrapContext = command.BootstrapContext; + + var frameworkServices = BootstrapContext.Advanced.FrameworkServices; + var hostBuilderContext = frameworkServices.GetRequiredService(); + var moduleAssemblyName = BootstrapContext.Module.GetType().Assembly.GetName().Name; + + var builder = WebApplication.CreateBuilder(new WebApplicationOptions + { + ApplicationName = moduleAssemblyName, + EnvironmentName = hostBuilderContext.HostingEnvironment.EnvironmentName, + ContentRootPath = hostBuilderContext.HostingEnvironment.ContentRootPath + }); + + // configuration: outer app configuration + module application name + module config filters. + builder.Configuration.AddConfiguration(hostBuilderContext.Configuration); + builder.Configuration.AddInMemoryCollection(new[] + { + new KeyValuePair(HostDefaults.ApplicationKey, moduleAssemblyName) + }); + + // a HostBuilderContext that reflects the *inner* module host (its environment and + // configuration), so module configuration/services filters see the module host being + // built rather than the outer host. + var moduleHostContext = new HostBuilderContext(new Dictionary()) + { + HostingEnvironment = builder.Environment, + Configuration = builder.Configuration + }; + + Filters.BuildFilterPipeline( + frameworkServices.GetServices(), + (_, __) => { })(BootstrapContext.ToModuleHostBuilderContext(moduleHostContext), builder.Configuration); + + // services: reuse the base pipeline (module services filters + module ConfigureServices, + // interface or convention). + // ReSharper disable once ConvertToUsingDeclaration + using (var tempServiceProvider = builder.Services.BuildServiceProvider()) + { + ConfigureServices(moduleHostContext, builder.Services, tempServiceProvider); + } + + // TODO: module static web assets (wwwroot / _content, namespaced as ".modules/{module}") + // are not yet mapped for the minimal-API host. The classic ModuleWebAssetsLoader is + // file-provider/XML-manifest based and does not fit the endpoint-based static web assets + // introduced in .NET 9. Tracked as a follow-up (see samples/dotnet/minimal/README.md). + + // let opt-in configurers touch the WebApplicationBuilder (e.g. Kestrel/urls). + foreach (var configurer in frameworkServices.GetServices()) + configurer.Configure(builder); + + // defer module Configure to pipeline build (start), after the container is configured. + builder.Services.AddSingleton(new ModuleConfigureStartupFilter(app => ConfigureApp(command, app))); + + // honor the module's service-provider validation override (parity with the classic host; + // see IModuleHostingOptions.ValidateServiceProvider). Without this the .NET 9+ Development + // default (ValidateOnBuild=true) can fail modules whose dependencies live in the container. + // NB: must be builder.Host (not builder.WebHost) - WebApplicationBuilder ignores the + // WebHost's service-provider factory but honors the Host's. + if (command.Options.HasServiceProviderValidationOverride()) + builder.Host.UseDefaultServiceProvider((_, options) => + ApplyServiceProviderValidation(options, command.Options)); + + command.Options.ConfigureBuilderAction?.Invoke(builder.Host); + + command.Host = builder.Build(); + command.ModuleContext = CreateModuleContext(command.Host.Services); + } + + private void ConfigureApp(BootstrapModuleHostCommand command, IApplicationBuilder app) + { + // middleware: the module's Configure (interface or convention). + Filters.BuildFilterPipeline( + BootstrapContext.Advanced.FrameworkServices.GetServices(), + (ctx, appBuilder) => + { + var innerContext = BootstrapContext.ToModuleContext(app.ApplicationServices); + if (innerContext.Module is IApplicationConfiguringModule configuringModule) + configuringModule.Configure(innerContext.ModulesHostServices, appBuilder); + else + ModuleMethodInvoker.CallOptionalMethod(innerContext, "Configure", appBuilder); + })(command.ModuleContext, app); + + // endpoints: minimal-API-style mapping via IEndpointConfiguringModule or a conventional + // MapEndpoints(IEndpointRouteBuilder) method (same interface-or-convention model as the + // other module methods). + var endpointContext = BootstrapContext.ToModuleContext(app.ApplicationServices); + var endpointModuleInstance = endpointContext.Module; + if (endpointModuleInstance is IEndpointConfiguringModule + || endpointModuleInstance.GetType().GetMethod("MapEndpoints") != null) + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + if (endpointModuleInstance is IEndpointConfiguringModule endpointModule) + endpointModule.MapEndpoints(endpointContext.ModulesHostServices, endpoints); + else + ModuleMethodInvoker.CallOptionalMethod(endpointContext, "MapEndpoints", endpoints); + }); + } + } + } +} +#endif diff --git a/src/Hosuto.Hosting.AspNetCore/Modules/Hosting/WebModuleBootstrapHostHandler.cs b/src/Hosuto.Hosting.AspNetCore/Modules/Hosting/WebModuleBootstrapHostHandler.cs index 0533a50..63638e7 100644 --- a/src/Hosuto.Hosting.AspNetCore/Modules/Hosting/WebModuleBootstrapHostHandler.cs +++ b/src/Hosuto.Hosting.AspNetCore/Modules/Hosting/WebModuleBootstrapHostHandler.cs @@ -31,6 +31,25 @@ protected virtual void Configure(IModuleContext moduleContext, IApplica })(moduleContext, app); +#if NETCOREAPP + // endpoints: minimal-API-style mapping via IEndpointConfiguringModule or a conventional + // MapEndpoints(IEndpointRouteBuilder) method (same contract as the minimal-API host). + // Endpoint routing is only available on ASP.NET Core 3.0+. + var endpointContext = BootstrapContext.ToModuleContext(app.ApplicationServices); + var endpointModuleInstance = endpointContext.Module; + if (endpointModuleInstance is IEndpointConfiguringModule + || endpointModuleInstance.GetType().GetMethod("MapEndpoints") != null) + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + if (endpointModuleInstance is IEndpointConfiguringModule endpointModule) + endpointModule.MapEndpoints(endpointContext.ModulesHostServices, endpoints); + else + ModuleMethodInvoker.CallOptionalMethod(endpointContext, "MapEndpoints", endpoints); + }); + } +#endif } public override void BootstrapHost(BootstrapModuleHostCommand command) diff --git a/src/Hosuto.Hosting.AspNetCore/Modules/IEndpointConfiguringModule.cs b/src/Hosuto.Hosting.AspNetCore/Modules/IEndpointConfiguringModule.cs new file mode 100644 index 0000000..4f957ba --- /dev/null +++ b/src/Hosuto.Hosting.AspNetCore/Modules/IEndpointConfiguringModule.cs @@ -0,0 +1,26 @@ +#if NETCOREAPP +using System; +using Microsoft.AspNetCore.Routing; + +namespace Dbosoft.Hosuto.Modules +{ + /// + /// Minimal-API-style module entry point for mapping endpoints. Implement this to map endpoints + /// the idiomatic way (endpoints.MapGet(...), endpoints.MapRazorPages(), + /// endpoints.MapControllers(), …) instead of the Startup-era + /// Configure(app => app.UseRouting()/app.UseEndpoints(...)). The framework sets up routing + /// and calls this within the endpoint middleware, so it works the same on the classic and the + /// minimal-API (UseAspNetCoreMinimal) host. A module may also implement + /// for non-endpoint middleware. + /// Equivalently, a module may expose a conventional MapEndpoints(IEndpointRouteBuilder) + /// method instead of implementing this interface (interface-or-convention, like the other + /// module methods). + /// + public interface IEndpointConfiguringModule + { + /// The modules host's service provider. + /// The endpoint route builder of the module's host. + void MapEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder endpoints); + } +} +#endif diff --git a/test/Hosuto.SimpleInjector.Tests/Modules/Hosting/WebApplicationHostTests.cs b/test/Hosuto.SimpleInjector.Tests/Modules/Hosting/WebApplicationHostTests.cs new file mode 100644 index 0000000..d49a4ea --- /dev/null +++ b/test/Hosuto.SimpleInjector.Tests/Modules/Hosting/WebApplicationHostTests.cs @@ -0,0 +1,208 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Dbosoft.Hosuto.Modules; +using Dbosoft.Hosuto.Modules.Hosting; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using SimpleInjector; +using Xunit; + +namespace Hosuto.SimpleInjector.Tests.Modules.Hosting +{ + public class WebApplicationHostTests + { + // A web module hosted on the opt-in minimal-API WebApplication inner host: module services + + // container (ConfigureContainer) + Configure/endpoint all work, and a container-only + // dependency is resolved by the mapped endpoint. + [Fact] + public async Task WebModule_on_minimal_web_application_serves_endpoint() + { + using var host = await StartMinimalHost(); + var body = await Get(host, BaseUrl(host) + "/greet"); + Assert.Equal("Hello from the module container", body); + await host.StopAsync(); + } + + // The minimal-API-native endpoint entry point: a module maps endpoints idiomatically via + // IEndpointConfiguringModule (endpoints.MapGet), no Startup-style UseRouting/UseEndpoints. + [Fact] + public async Task WebModule_maps_endpoints_via_IEndpointConfiguringModule() + { + using var host = await StartMinimalHost(); + var body = await Get(host, BaseUrl(host) + "/hello"); + Assert.Equal("mapped via IEndpointConfiguringModule", body); + await host.StopAsync(); + } + + // Endpoints are also mappable via a conventional MapEndpoints method (no interface), + // consistent with the other module methods' interface-or-convention model. + [Fact] + public async Task WebModule_maps_endpoints_via_convention_method() + { + using var host = await StartMinimalHost(); + var body = await Get(host, BaseUrl(host) + "/conv"); + Assert.Equal("mapped via convention", body); + await host.StopAsync(); + } + +#if NET9_0_OR_GREATER + // On .NET 9+ the minimal host enables ValidateOnBuild in Development, so a container-only + // dependency fails validation - unless the module opts out via ValidateServiceProvider. + [Fact] + public void Minimal_in_development_without_override_fails_validation() + { + var builder = ModulesHost.CreateDefaultBuilder(); + builder.UseSimpleInjector(new Container()); + builder.UseEnvironment("Development"); + builder.UseAspNetCoreMinimal(app => app.WebHost.UseUrls("http://127.0.0.1:0")); + builder.HostModule(); + + Assert.ThrowsAny(() => builder.Build().Dispose()); + } + + [Fact] + public void Minimal_ValidateServiceProvider_can_disable_build_validation() + { + var builder = ModulesHost.CreateDefaultBuilder(); + builder.UseSimpleInjector(new Container()); + builder.UseEnvironment("Development"); + builder.UseAspNetCoreMinimal(app => app.WebHost.UseUrls("http://127.0.0.1:0")); + builder.HostModule( + options => options.ValidateServiceProvider(validateScopes: false, validateOnBuild: false)); + + builder.Build().Dispose(); + } +#endif + + private static async Task StartMinimalHost() where TModule : class + { + var builder = ModulesHost.CreateDefaultBuilder(); + builder.UseSimpleInjector(new Container()); + builder.UseAspNetCoreMinimal(app => app.WebHost.UseUrls("http://127.0.0.1:0")); + builder.HostModule(); + + var host = builder.Build(); + await host.StartAsync(); + return host; + } + + private static string BaseUrl(IHost host) where TModule : class => + host.Services.GetRequiredService>().Services + .GetRequiredService().Features.Get().Addresses.First(); + + private static async Task Get(IHost host, string url) + { + using var http = new HttpClient(); + return await http.GetStringAsync(url); + } + + public interface IGreeter + { + string Greet(); + } + + public sealed class Greeter : IGreeter + { + public string Greet() => "Hello from the module container"; + } + + public sealed class GreeterWebModule : WebModule, IContainerConfiguringModule, IApplicationConfiguringModule + { + public override string Path { get; } = ""; + + private Container _container; + + // ReSharper disable once UnusedMember.Global + public void ConfigureServices(IServiceCollection services) + { + services.AddRouting(); + } + + void IContainerConfiguringModule.ConfigureContainer(IServiceProvider serviceProvider, Container container) + { + container.Register(Lifestyle.Singleton); + _container = container; + } + + void IApplicationConfiguringModule.Configure(IServiceProvider serviceProvider, IApplicationBuilder app) + { + app.UseRouting(); + app.UseEndpoints(endpoints => + endpoints.MapGet("/greet", async context => + await context.Response.WriteAsync(_container.GetInstance().Greet()))); + } + } + + public sealed class ConventionEndpointModule : WebModule, IServiceConfiguringModule + { + public override string Path { get; } = ""; + + void IServiceConfiguringModule.ConfigureServices(IServiceProvider serviceProvider, IServiceCollection services) + { + services.AddRouting(); + } + + // conventional MapEndpoints (no IEndpointConfiguringModule) - discovered by reflection. + // ReSharper disable once UnusedMember.Global + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + endpoints.MapGet("/conv", () => "mapped via convention"); + } + } + + public sealed class EndpointModule : WebModule, IServiceConfiguringModule, IEndpointConfiguringModule + { + public override string Path { get; } = ""; + + void IServiceConfiguringModule.ConfigureServices(IServiceProvider serviceProvider, IServiceCollection services) + { + services.AddRouting(); + } + + void IEndpointConfiguringModule.MapEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder endpoints) + { + endpoints.MapGet("/hello", () => "mapped via IEndpointConfiguringModule"); + } + } + + public interface IBus + { + } + + public sealed class FakeBus : IBus + { + } + + public sealed class NeedsBus + { + public NeedsBus(IBus bus) + { + _ = bus; + } + } + + // impl-type singleton whose dependency lives only in the module container. + public sealed class BrokenValidationModule : WebModule, IServiceConfiguringModule, IContainerConfiguringModule + { + public override string Path { get; } = ""; + + void IServiceConfiguringModule.ConfigureServices(IServiceProvider serviceProvider, IServiceCollection services) + { + services.AddSingleton(); + } + + void IContainerConfiguringModule.ConfigureContainer(IServiceProvider serviceProvider, Container container) + { + container.RegisterInstance(new FakeBus()); + } + } + } +}