Fix Test Tool v2 Blazor UI non-interactive on .NET 10 (static assets 404) - #2492
Fix Test Tool v2 Blazor UI non-interactive on .NET 10 (static assets 404)#2492GarrettBeatty wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes AWS Lambda Test Tool v2’s Blazor UI becoming non-interactive on .NET 9+/net10 by ensuring the .NET 9+ static web assets endpoint pipeline can actually locate and serve framework/scoped-CSS assets regardless of launch working directory and hosting environment.
Changes:
- Pins ASP.NET Core content root to
AppContext.BaseDirectorysoWebRootFileProviderresolveswwwrootcorrectly when the tool is launched from an arbitrary cwd (global tool scenario). - On
NET9_0_OR_GREATER, composes static web assets (UseStaticWebAssets) and maps endpoint-based static assets (MapStaticAssets) with a guard to avoid failures when running under a test host. - Adds an Autover patch changelog entry documenting the fix.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs | Adjusts hosting options and .NET 9+ static web assets setup so Blazor framework/scoped CSS assets resolve and the UI becomes interactive again. |
| .autover/changes/156dd8b1-1af2-45fc-b051-b94dc51fc56f.json | Records a patch-level changelog entry for the .NET 9+/net10 static assets/Blazor interactivity fix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Replace the Test Tool durable emulator's hand-maintained fork of the checkpoint state machine and operation store with the shared Amazon.Lambda.DurableExecution.LocalEmulation package, so it cannot drift from the durable-execution testing package. - Delete the forked CheckpointProcessor and InMemoryOperationStore. - Add WireOperationUpdateMapper (STJ wire DTO -> OperationUpdateInput) and route DurableExecutionStore/DurableExecutionDriver through the kernel's processor, store, exec-0 seeding, and resume-delay helper. - Switch the durable SDK reference from a preview PackageReference to an in-repo source ProjectReference (matching how RuntimeSupport is referenced) so there is a single Amazon.Lambda.DurableExecution assembly identity across the SDK and the kernel. Stacked on the LocalEmulation kernel PR (and PR #2492); builds once both are on the target branch. No user-facing behavior change.
…404) On .NET 9+ the Blazor framework files (_framework/blazor.web.js) and the scoped-CSS bundle are served through the endpoint-routing MapStaticAssets API backed by the *.staticwebassets.endpoints.json manifest, not the classic static-files middleware. The tool only registered a wwwroot-only UseStaticFiles provider, so on net10 those assets returned 404, window.Blazor was never defined, no interactive server circuit was established, and the whole UI rendered statically (buttons/@onclick/@Bind non-functional). Add a NET9_0_OR_GREATER-guarded app.MapStaticAssets() so net9+ serves the framework + scoped assets via the endpoints manifest while net8.0 keeps its existing path. Also disable the build-manifest dev-time runtime-patching handler (ReloadStaticAssetsAtRuntime=false), which otherwise probes those assets through the physical wwwroot provider and threw FileNotFoundException (HTTP 500) for _framework/* under dotnet run/build. Serve net10 Blazor framework assets with real bytes (fix 0-byte 200 responses) The previous fix made _framework/blazor.web.js return HTTP 200 but with an empty (0-byte) body, so window.Blazor was still never defined and the UI stayed non-interactive. Two root causes, both fixed here: 1. MapStaticAssets serves asset bytes from IWebHostEnvironment.WebRootFileProvider. Under dotnet run the framework files live in the NuGet cache and are mapped in via *.staticwebassets.runtime.json, but ASP.NET Core only composes that manifest into the web root automatically in the Development environment. The tool runs as Production by default, so the web root was a bare wwwroot provider and the invoker caught FileNotFoundException and returned an empty 200. Fixed by calling builder.WebHost.UseStaticWebAssets(), which composes the manifest regardless of environment (and is a harmless no-op for the installed tool, where the framework files are published directly into wwwroot). 2. As an installed global tool the process launches from an arbitrary working directory, and the default content root (hence WebRootFileProvider = contentRoot/wwwroot) followed the cwd, so MapStaticAssets looked in a nonexistent wwwroot and returned empty 200s for every asset. Fixed by pinning ContentRootPath to AppContext.BaseDirectory. Replaces the earlier ReloadStaticAssetsAtRuntime=false workaround, which only suppressed the dev hot-reload 500 but left MapStaticAssets serving 0 bytes. Verified on net10 with byte counts and a real browser (Playwright window.Blazor): - dotnet run: blazor.web.js=200575 bytes, styles.css=2024, bootstrap=232808; window.Blazor=true on / and /documentation. - installed global tool launched from a foreign cwd: same byte counts; window.Blazor=true on / and /documentation. net8 still builds and stays interactive (window.Blazor=true). Only MapStaticAssets when the manifest exists (fix net10 unit tests) TestToolProcess.Startup is invoked directly by unit tests (RuntimeApiTests, RunCommandTests) inside the xUnit test host. MapStaticAssets() throws InvalidOperationException when the *.staticwebassets.endpoints.json manifest is absent, and the manifest is named after the entry assembly — under the test host that's 'testhost', so the tool's manifest isn't present and every test that calls Startup failed on net10 (net8 was unaffected as it doesn't call MapStaticAssets). Guard the call so it only maps static assets when the expected manifest exists: the running tool has it (assets serve, UI is interactive); the test host does not (skipped, and those tests only exercise the Runtime API). Verified: the previously-failing net10 tests pass, and the real tool still serves _framework/blazor.web.js with real bytes. Fix RCL _content/** assets 404 on .NET 8 (dotnet run) The Blazor web UI's static-asset serving was only fully fixed for .NET 9+. On .NET 8, running from a build output (dotnet run / dotnet build) left Razor class library content — notably BlazorMonaco's _content/BlazorMonaco/** editor assets — returning 404, so the request/response code editor never initialized and selecting an example request could not populate the input. Root cause: those RCL assets are not physically under wwwroot in a build output; they are surfaced through the static-web-assets runtime manifest. UseStaticWebAssets() (which composes that manifest into the WebRootFileProvider) was guarded to net9+, and the classic UseStaticFiles middleware was pinned to an explicit bare-wwwroot provider that never reads the composed manifest. On net9+ MapStaticAssets reads the WebRootFileProvider so it worked there; net8 had no such reader. Fix: - Compose the static-web-assets manifest on all target frameworks (make UseStaticWebAssets() unconditional). - On .NET 8, add a second UseStaticFiles pass over the WebRootFileProvider so manifest-mapped _content/** assets resolve. For an installed global tool the WebRootFileProvider is the same wwwroot (assets published there directly), so it is a harmless second lookup. Verified across the full matrix — .NET 8/.NET 10 x Development/Production x dotnet-run/installed-tool (8/8 serve _framework, _content/BlazorMonaco, and app.css with non-zero bytes).
4e578f1 to
5b49dc2
Compare
|
i retested a second time now manually here is what i did. Packed and installed the tool to and then for each TFM (net 8/10), i launched the tool with dev mode prod mode (if environment is unset the default is prod as per microsfot docs) unset ASPNETCORE_ENVIRONMENT; dotnet start amd then verified in the ui triggering the function it works |
| // into wwwroot. Pinning an explicit provider (rather than the default WebRootFileProvider) | ||
| // also avoids depending on the static-web-assets manifest, whose absolute build-machine | ||
| // paths would not resolve on another machine. | ||
| app.UseStaticFiles(new StaticFileOptions |
There was a problem hiding this comment.
Given the calls to UseStaticFiles and MapStaticAssets done below is the call still needed?
There was a problem hiding this comment.
UseStaticWebAssets()— tells the server where the framework/Monaco files actually are (the NuGet cache, via the manifest). Without it the server looks only inwwwroot, doesn't find them, and
everything below has nothing to serve. This is the prerequisite for the other two.UseStaticFiles(wwwroot)— serves the plain files that do live inwwwroot:app.css, images, favicon. Nothing else serves these, so without it the page loads unstyled.MapStaticAssets()(net9+) — on .NET 9+ the framework files (blazor.web.js) and scoped CSS are served through this newer pipeline, not the classic one. Without it they come back empty,
window.Blazornever loads, and the UI is dead. (.NET 8 has noMapStaticAssets, so a secondUseStaticFilesover the manifest-composed provider does the same job.)
tried moving some of the code around and updating the comments to be easier to understand. but i tried testing without all of these options and some scenarios did not work
There was a problem hiding this comment.
Legend for asset kinds:
- classic =
app.css, images, favicon (physically in wwwroot) - framework =
_framework/blazor.web.js+ scoped CSS (needed for interactivity) - RCL =
_content/BlazorMonaco/**(needed for the code editor)
| Framework | Deployment | classic served by | framework served by | RCL (Monaco) served by | Extra thing that makes it work |
|---|---|---|---|---|---|
| net10 | dotnet run |
UseStaticFiles(wwwroot) |
MapStaticAssets() |
MapStaticAssets() |
UseStaticWebAssets() — files are in the NuGet cache, not wwwroot |
| net10 | installed tool | UseStaticFiles(wwwroot) |
MapStaticAssets() |
MapStaticAssets() |
ContentRootPath pin — tool launches from an arbitrary cwd |
| net8 | dotnet run |
UseStaticFiles(wwwroot) |
Blazor's own middleware | 2nd UseStaticFiles(WebRoot) |
UseStaticWebAssets() — files are in the NuGet cache, not wwwroot |
| net8 | installed tool | UseStaticFiles(wwwroot) |
Blazor's own middleware | UseStaticFiles(wwwroot) |
ContentRootPath pin — everything's in wwwroot, so the base call covers it |
…ndling
Address PR feedback on the three static-file registrations. No functional
change: same UseStaticFiles/MapStaticAssets calls and guards, verified green
across the full {net8,net10} x {Production,Development} x {dotnet run,installed}
matrix.
- Trim the verbose per-block comments (~54 -> ~28 lines) while keeping the facts.
- Merge the separate #if !NET9 / #if NET9+ regions into one #if/#else placed
right after the base UseStaticFiles, framing static assets as two layers: a
manifest-independent base (classic wwwroot) plus per-TFM handling for the
manifest-mapped framework/scoped-CSS/RCL _content assets.

Symptom
The AWS Lambda Test Tool v2 Blazor web UI had two static-asset serving bugs that broke interactivity, each specific to a target framework:
(A) Non-interactive on .NET 10 (any .NET 9+). Buttons don't respond,
@onclick/@bindnever fire;window.Blazorisundefined. The tell is subtle — the framework script returns HTTP 200 with a 0-byte body, so status-code checks alone look green while the browser downloads nothing:(B) Code editor dead on .NET 8 (
dotnet run). The Razor class library assets for the Monaco code editor (_content/BlazorMonaco/**) return 404, so the request/response editor never initializes — e.g. selecting an example request does not populate the input, and typing/formatting in the editor doesn't work:Root cause
Static-asset serving changed in .NET 9: framework assets (
_framework/*) and scoped CSS are served by the endpoint-routingapp.MapStaticAssets()pipeline, which reads bytes fromIWebHostEnvironment.WebRootFileProvider. Underdotnet run, neither the framework files nor RCL_content/**assets physically live inwwwroot— they live in the NuGet cache and are surfaced through the*.staticwebassets.runtime.jsonmanifest. Two things kept that manifest from being reachable:Manifest not composed outside Development. ASP.NET Core only composes the static-web-assets manifest into
WebRootFileProviderautomatically when the environment is Development (WebHost.ConfigureWebDefaultscallsStaticWebAssetsLoader.UseStaticWebAssetsonly underIsDevelopment()). This tool runs as Production by default, so the web root was a barewwwrootprovider.MapStaticAssetsserved empty (0-byte) 200s for_framework/*→ symptom (A).MapStaticAssets), the manifest was likewise never composed, and — see below — nothing read it anyway → symptom (B).Content root followed the current working directory. As an installed global tool the process launches from an arbitrary cwd;
WebRootFileProviderdefaulted tocwd/wwwroot, a nonexistent path, so every asset returned empty 200s.The classic
UseStaticFilesused an explicit bare-wwwroot provider. It was pinned to aPhysicalFileProvider(wwwroot)and never read the manifest-composedWebRootFileProvider. On net9+ this was masked becauseMapStaticAssetsreadsWebRootFileProvider; on net8 there is no such reader, so manifest-mapped_content/**assets were unreachable → symptom (B).Fix
All in
Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs:WebRootFileProviderresolves regardless of launching cwd:_framework/*+ scoped CSS via the endpoints manifest on net9+:UseStaticFilesoverWebRootFileProviderso manifest-mapped RCL_content/**assets (BlazorMonaco) resolve. There is noMapStaticAssetson net8, and the explicit wwwroot provider can't see the composed manifest:The existing wwwroot
UseStaticFilesprovider is preserved (authoritative for the installed tool, where all assets are published directly intowwwroot). For an installed tool the second net8 lookup resolves to the samewwwroot, so it is a harmless second pass.Verification — full 8-case matrix
Probed the three assets whose absence breaks the UI —
_framework/blazor.web.js(interactive circuit),_content/BlazorMonaco/.../loader.js(Monaco editor → example-request fill), andapp.css— asserting HTTP 200 with non-zero bytes across every combination of {.NET 8, .NET 10} × {Production, Development} × {dotnet run, installed tool}:(Before this change:
run/net8.0/*hadmonaco loader.js -> 404andrun/net10.0/*hadblazor.web.js -> 200, 0 bytes.) Real-browser confirmation via headless Chromium:window.Blazor=trueand no console errors on/,/documentation, and/durable-execution. Both TFMs build with 0 warnings / 0 errors; the Test Tool unit tests (which invokeStartupdirectly) pass on net8.