Skip to content

Add opt-in minimal-API (WebApplication) inner host for web modules - #43

Merged
fw2568 merged 3 commits into
mainfrom
feature/webapplication-host
Jul 21, 2026
Merged

Add opt-in minimal-API (WebApplication) inner host for web modules#43
fw2568 merged 3 commits into
mainfrom
feature/webapplication-host

Conversation

@fw2568

@fw2568 fw2568 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Opt-in, additive. The classic ConfigureWebHostDefaults host stays the default; existing modules and apps are unaffected.

What

UseAspNetCoreMinimal() hosts a web module on a minimal-API WebApplication inner host (net6+) instead of HostBuilder + 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):

  • ConfigureServices runs against WebApplicationBuilder.Services before build.
  • Configure is deferred to request-pipeline build (start) via an IStartupFilter, so it runs after the module container (SimpleInjector/Autofac) has been configured by the bootstrap pipeline.
  • The SimpleInjector bootstrap filter composes unchanged, because WebApplication is an IHost (UseSimpleInjector + ConfigureContainer + Verify still run post-build).
  • IWebApplicationBuilderConfigurer is the WebApplication-native opt-in hook (e.g. Kestrel/urls), replacing the IWebHostBuilder-based hooks that don't apply to WebApplicationBuilder.

Tests

WebApplicationHostTests runs 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 classic ModuleWebAssetsLoader is 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 (see samples/dotnet/minimal/README.md and the TODO in WebApplicationModuleHostHandler).

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 via WebApplicationBuilder.
  • Defers module Configure(...) execution to pipeline build/start using an IStartupFilter to preserve post-build container configuration behavior.
  • Adds WebApplicationHostTests and a samples/dotnet/minimal sample 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.

Comment on lines +39 to +51
// 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);
}
Comment on lines +62 to +66
// 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.
@fw2568

fw2568 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the Copilot + Sonnet review feedback and added the endpoint contract (1a5977b):

New — IEndpointConfiguringModule (from the review discussion on minimal-API fit): modules can map endpoints idiomatically (endpoints.MapGet/MapRazorPages/MapControllers) instead of Startup-era Configure(app => UseRouting/UseEndpoints). Wired into both the minimal-API and the classic web host, so the same module works on either backend. Configure(IApplicationBuilder) remains for middleware.

Review fixes (minimal host):

  • ValidateServiceProvider now honored on the minimal path — it was dropped, reintroducing the Fix hosted handler failing ValidateOnBuild on .NET 9+ #40 ValidateOnBuild failure in Development on .NET 9+. Applied via builder.Host.UseDefaultServiceProvider; I verified empirically that builder.WebHost's factory is ignored by WebApplicationBuilder whereas builder.Host is honored (so the reviews' concern was real, and the obvious WebHost fix would have been a silent no-op).
  • Inner HostBuilderContext: module services/configuration filters now see the inner module host's environment + configuration, not the outer host's.
  • HostDefaults.ApplicationKey added to module configuration for parity with the classic host.
  • Documented that 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.

Not addressed (by design / follow-up): the both-UseAspNetCore*-registered guard (documented limitation — one web-host backend per builder) and WebApplicationOptions.Args (parity with the classic host, which also doesn't pass args to the inner host). Module static web assets remain the separate follow-up (already solved on a branch).

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.
@fw2568
fw2568 merged commit f81fb36 into main Jul 21, 2026
3 checks passed
@fw2568
fw2568 deleted the feature/webapplication-host branch July 21, 2026 20:44
fw2568 added a commit that referenced this pull request Jul 21, 2026
* 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants