Skip to content

Serve module static web assets on the minimal-API host - #44

Merged
fw2568 merged 2 commits into
mainfrom
fix/module-static-web-assets
Jul 21, 2026
Merged

Serve module static web assets on the minimal-API host#44
fw2568 merged 2 commits into
mainfrom
fix/module-static-web-assets

Conversation

@fw2568

@fw2568 fw2568 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Resolves the follow-up left by #43: a web module hosted via UseAspNetCoreMinimal() now serves its own static web assets (wwwroot / _content), on net6/8/9/10, in both dotnet run (dev) and dotnet publish output.

Approach

Each module's inner WebApplication sets 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:

  • StaticWebAssetsLoader.UseStaticWebAssets — dev (reads {module}.staticwebassets.runtime.json)
  • ModuleWebAssetsLoader.UseModuleAssets — published ({contentRoot}/wwwroot/.modules/{module})

No .modules/{module} prefix stripping is needed and each module host serves only its own assets (cleaner isolation than the classic shared-host filtering). Assets are served through the classic UseStaticFiles pipeline; the WebRootFileProvider is resolved lazily by the middleware after build.

Why not MapStaticAssets (endpoint-based): the module serves via UseStaticFiles, the endpoint routes are fingerprinted, and scoping per-module endpoints into a sub-host is more invasive. The file-provider path is simpler and matches how Hosuto modules have always served assets.

Verification

samples/dotnet/minimal: GET /css/site.css200 in dev and in published output (GET / stays 200). README updated to reflect the now-working behavior. All existing suites green on net6/8/9.

Note: an automated static-web-assets test needs a referenced Razor library with build-generated manifests; the sample serves as the end-to-end validation here.

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.

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

Enables Hosuto web modules hosted via UseAspNetCoreMinimal() to serve their own static web assets (wwwroot / _content) from the module’s inner WebApplication, aligning minimal-host behavior with the classic host across dev (dotnet run) and publish outputs.

Changes:

  • Load and compose module static web assets onto the inner host’s WebRootFileProvider after WebApplication.Build().
  • Update the minimal sample README to reflect that module static assets now resolve (200) in both dev and published output.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/Hosuto.Hosting.AspNetCore/Modules/Hosting/WebApplicationModuleHostHandler.cs Adds post-build static web asset composition for minimal-hosted modules.
samples/dotnet/minimal/README.md Updates sample documentation to describe the now-working static asset behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +108 to +119
private static void MapModuleStaticWebAssets(IServiceProvider hostServices)
{
var environment = hostServices.GetService<IWebHostEnvironment>();
var configuration = hostServices.GetService<IConfiguration>();
if (environment == null || configuration == null)
return;

// dev: reads {module}.staticwebassets.runtime.json (assets mapped at the web root).
StaticWebAssetsLoader.UseStaticWebAssets(environment, configuration);
// published: composes {contentRoot}/wwwroot/.modules/{module} physically.
ModuleWebAssets.ModuleWebAssetsLoader.UseModuleAssets(environment, configuration);
}
Comment on lines 98 to 100
command.Host = builder.Build();
MapModuleStaticWebAssets(command.Host.Services);
command.ModuleContext = CreateModuleContext(command.Host.Services);
- 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.
@fw2568

fw2568 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — addressed in 2d776eb:

Automated test (your strongest point): added WebModule_serves_its_static_web_assets — it populates a temp {contentRoot}/wwwroot/.modules/{module} and asserts the asset is served over HTTP through the module host. Verified it fails if the asset composition is removed, so it guards the behavior.

Fail-fast: switched to GetRequiredService for the host environment/configuration (they're always registered on a WebApplication), so no silent no-op.

Dev-vs-non-dev gating: I intentionally kept UseStaticWebAssets ungated and documented why. Gating on IsDevelopment() (as the classic host does) would reintroduce the 404: a modules host defaults to the Production environment under dotnet run, so the dev manifest would never load. The two loaders are each a no-op when their input is absent (dev/build-output manifest vs. published .modules/{module} physical layout), so calling both unconditionally is safe; in real published deployments there is no runtime manifest, so only the physical path applies.

@fw2568
fw2568 merged commit 7c98a4f into main Jul 21, 2026
2 of 3 checks passed
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