Add opt-in minimal-API (WebApplication) inner host for web modules - #43
Conversation
New UseAspNetCoreMinimal() hosts a web module on a minimal-API WebApplication instead of the classic HostBuilder + ConfigureWebHostDefaults host (net6+, additive - the existing path stays the default). The module authoring model is unchanged: ConfigureServices runs against builder.Services pre-build, and Configure is deferred to pipeline build (start) via an IStartupFilter so it runs after the module container (SimpleInjector/Autofac) has been configured. The SimpleInjector bootstrap filter composes unchanged because WebApplication is an IHost. Builds on the module interfaces from #42, with reflection convention as fallback. Adds a framework test (module services + container + endpoint with a container-resolved dependency, over the real bootstrap path) and a net10 Razor sample under samples/dotnet/minimal. Known limitation (documented, follow-up): module static web assets are not yet mapped on the minimal host - the classic file-provider/XML-manifest loader does not fit .NET 9+ endpoint-based static web assets.
There was a problem hiding this comment.
Pull request overview
Adds an opt-in hosting path that runs ASP.NET Core web modules on a minimal-API WebApplication inner host (net6+) instead of the classic HostBuilder + ConfigureWebHostDefaults approach, plus a sample and an end-to-end SimpleInjector test to validate the new bootstrap flow.
Changes:
- Introduces
UseAspNetCoreMinimal()and supporting bootstrap/handler types to build a module inner host viaWebApplicationBuilder. - Defers module
Configure(...)execution to pipeline build/start using anIStartupFilterto preserve post-build container configuration behavior. - Adds
WebApplicationHostTestsand asamples/dotnet/minimalsample demonstrating Razor Pages on the minimal inner host (with documented static-assets limitation).
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/Hosuto.SimpleInjector.Tests/Modules/Hosting/WebApplicationHostTests.cs | New end-to-end test covering minimal inner host + container dependency resolution through an endpoint |
| src/Hosuto.Hosting.AspNetCore/Modules/Hosting/WebApplicationModuleHostHandler.cs | New minimal-host inner handler for web modules (WebApplication-based) |
| src/Hosuto.Hosting.AspNetCore/Modules/Hosting/WebApplicationBootstrapHostFilter.cs | New bootstrap filter wiring the minimal-host handler into the bootstrap pipeline |
| src/Hosuto.Hosting.AspNetCore/Modules/Hosting/ModulesHostBuilderExtensions.cs | Adds UseAspNetCoreMinimal() extension to opt into the new hosting path |
| src/Hosuto.Hosting.AspNetCore/Modules/Hosting/ModuleConfigureStartupFilter.cs | Adds startup filter used to run module Configure(...) during pipeline build/start + configurer hook interface |
| samples/dotnet/minimal/README.md | Documentation for the minimal-host sample and the known static-assets limitation |
| samples/dotnet/minimal/RazorModule/wwwroot/css/site.css | Sample static asset (expected 404 currently per known limitation) |
| samples/dotnet/minimal/RazorModule/RazorModule.csproj | Razor module sample project (net10) with namespaced static web assets base path |
| samples/dotnet/minimal/RazorModule/RazorGreetModule.cs | Sample web module using Razor Pages on the minimal inner host |
| samples/dotnet/minimal/RazorModule/Pages/Index.cshtml | Sample Razor page rendered by the module |
| samples/dotnet/minimal/App/Program.cs | Sample host app that starts the module and probes endpoints |
| samples/dotnet/minimal/App/App.csproj | Sample host app project (net10) referencing the Razor module |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // configuration: outer app configuration + module configuration filters. | ||
| builder.Configuration.AddConfiguration(hostBuilderContext.Configuration); | ||
| Filters.BuildFilterPipeline( | ||
| frameworkServices.GetServices<IModuleConfigurationFilter>(), | ||
| (_, __) => { })(BootstrapContext.ToModuleHostBuilderContext(hostBuilderContext), 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(hostBuilderContext, builder.Services, tempServiceProvider); | ||
| } |
| // defer module Configure to pipeline build (start), after the container is configured. | ||
| builder.Services.AddSingleton<IStartupFilter>(new ModuleConfigureStartupFilter(app => ConfigureApp(command, app))); | ||
|
|
||
| command.Options.ConfigureBuilderAction?.Invoke(builder.Host); | ||
|
|
Endpoint contract (from review discussion): IEndpointConfiguringModule lets a module map endpoints the minimal-API way (endpoints.MapGet/MapRazorPages/...), wired into both the minimal-API and classic web hosts so the same module works on either backend. The Startup-era Configure(IApplicationBuilder) stays for middleware. Review fixes for the minimal host (Copilot + Sonnet): - Honor IModuleHostingOptions.ValidateServiceProvider on the minimal path (was dropped, reintroducing the #40 ValidateOnBuild failure in Development on .NET 9+). Applied via builder.Host.UseDefaultServiceProvider - verified that builder.WebHost's factory is ignored by WebApplicationBuilder while builder.Host is honored. - Pass a HostBuilderContext reflecting the inner module host (its environment + configuration) to the module services/configuration filters, instead of the outer host context. - Add HostDefaults.ApplicationKey to the module configuration for parity with the classic host. - Doc: note ConfigureBuilderAction runs against the restricted WebApplicationBuilder.Host. Tests: idiomatic IEndpointConfiguringModule mapping over the real minimal path, plus ValidateServiceProvider on/off (net9+). Sample updated to author the module with all three contracts (services/middleware/endpoints). Green on net6/8/9.
|
Addressed the Copilot + Sonnet review feedback and added the endpoint contract (1a5977b): New — Review fixes (minimal host):
Tests: idiomatic Not addressed (by design / follow-up): the both- |
CI: add the net10 samples/dotnet/minimal projects to Hosuto.sln. The pipeline restores the solution and then builds **/*.csproj --no-restore; projects missing from the solution were never restored and failed with NETSDK1004. MapEndpoints: dispatch also to a conventional MapEndpoints(IEndpointRouteBuilder) method when the module does not implement IEndpointConfiguringModule, matching the interface-or-convention model of the other module methods (both web hosts). Adds a convention-based endpoint test.
* Serve module static web assets on the minimal-API host Resolve the follow-up left by #43: a module hosted via UseAspNetCoreMinimal now serves its own static web assets (wwwroot / _content), on net6/8/9/10, in both dev (dotnet run) and published output. Each module's inner WebApplication has ApplicationName = the module assembly, so the module's own static web assets manifest is already root-mapped. After Build() the handler composes those assets onto the module's WebRootFileProvider via StaticWebAssetsLoader.UseStaticWebAssets (dev) and ModuleWebAssetsLoader.UseModuleAssets (published) - no ".modules/{module}" prefix stripping, and each module host serves only its own assets. Assets are served through the classic UseStaticFiles pipeline (WebRootFileProvider resolved lazily by the middleware after build). Verified against samples/dotnet/minimal: GET /css/site.css -> 200 in dev and publish. * Add static-web-assets test; fail-fast + document env gating (review) - Add an integration test that serves a module's static web asset over HTTP (published .modules/{module} layout in a temp content root) - verified to fail if the asset composition is removed. - Fail fast (GetRequiredService) instead of silently no-op'ing when the host env or configuration is missing. - Document why UseStaticWebAssets is deliberately NOT gated on IsDevelopment: a modules host defaults to Production under `dotnet run`, so an env gate would never load the dev manifest and module assets would 404; both loaders are no-ops when their input is absent.
Opt-in, additive. The classic
ConfigureWebHostDefaultshost stays the default; existing modules and apps are unaffected.What
UseAspNetCoreMinimal()hosts a web module on a minimal-APIWebApplicationinner host (net6+) instead ofHostBuilder + ConfigureWebHostDefaults. This is the internal step toward converging single- and multi-host on one modern host backend, and the basis for trimming/AOT-friendly module hosts.The module authoring model is unchanged (convention or the #42 interfaces):
ConfigureServicesruns againstWebApplicationBuilder.Servicesbefore build.Configureis deferred to request-pipeline build (start) via anIStartupFilter, so it runs after the module container (SimpleInjector/Autofac) has been configured by the bootstrap pipeline.WebApplicationis anIHost(UseSimpleInjector+ConfigureContainer+Verifystill run post-build).IWebApplicationBuilderConfigureris the WebApplication-native opt-in hook (e.g. Kestrel/urls), replacing theIWebHostBuilder-based hooks that don't apply toWebApplicationBuilder.Tests
WebApplicationHostTestsruns through the real bootstrap path (ModulesHost … UseSimpleInjector … UseAspNetCoreMinimal … HostModule<T>), starts the host, and asserts a mapped endpoint resolves a container-only dependency. Green on net6.0/8.0/9.0.Sample
samples/dotnet/minimal— a net10 Razor module on the minimal host. Razor Pages render (200); it also documents the one known limitation below.Known limitation (follow-up, not in this PR)
Module static web assets are not yet mapped on the minimal host (
GET /css/site.css→ 404 in the sample). The classicModuleWebAssetsLoaderis file-provider/XML-manifest based and does not fit .NET 9+ endpoint-based static web assets; mapping per-module assets on net9/10 is a separate task (seesamples/dotnet/minimal/README.mdand the TODO inWebApplicationModuleHostHandler).