Skip to content

Fix Test Tool v2 Blazor UI non-interactive on .NET 10 (static assets 404)#2492

Draft
GarrettBeatty wants to merge 4 commits into
devfrom
fix/testtool-net10-static-assets
Draft

Fix Test Tool v2 Blazor UI non-interactive on .NET 10 (static assets 404)#2492
GarrettBeatty wants to merge 4 commits into
devfrom
fix/testtool-net10-static-assets

Conversation

@GarrettBeatty

@GarrettBeatty GarrettBeatty commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

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/@bind never fire; window.Blazor is undefined. 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:

# net10, before this fix
_framework/blazor.web.js          -> HTTP 200, 0 bytes      <-- EMPTY
Amazon.Lambda.TestTool.styles.css -> HTTP 200, 0 bytes      <-- EMPTY

(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:

# net8, dotnet run, before this fix
_framework/blazor.web.js                              -> HTTP 200, 187162 bytes  (ok)
_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js -> HTTP 404, 0 bytes    <-- MISSING

Root cause

Static-asset serving changed in .NET 9: framework assets (_framework/*) and scoped CSS are served by the endpoint-routing app.MapStaticAssets() pipeline, which reads bytes from IWebHostEnvironment.WebRootFileProvider. Under dotnet run, neither the framework files nor RCL _content/** assets physically live in wwwroot — they live in the NuGet cache and are surfaced through the *.staticwebassets.runtime.json manifest. Two things kept that manifest from being reachable:

  1. Manifest not composed outside Development. ASP.NET Core only composes the static-web-assets manifest into WebRootFileProvider automatically when the environment is Development (WebHost.ConfigureWebDefaults calls StaticWebAssetsLoader.UseStaticWebAssets only under IsDevelopment()). This tool runs as Production by default, so the web root was a bare wwwroot provider.

    • On net9+ this meant MapStaticAssets served empty (0-byte) 200s for _framework/* → symptom (A).
    • On net8 (no MapStaticAssets), the manifest was likewise never composed, and — see below — nothing read it anyway → symptom (B).
  2. Content root followed the current working directory. As an installed global tool the process launches from an arbitrary cwd; WebRootFileProvider defaulted to cwd/wwwroot, a nonexistent path, so every asset returned empty 200s.

  3. The classic UseStaticFiles used an explicit bare-wwwroot provider. It was pinned to a PhysicalFileProvider(wwwroot) and never read the manifest-composed WebRootFileProvider. On net9+ this was masked because MapStaticAssets reads WebRootFileProvider; 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:

  • Pin the content root to the install dir so WebRootFileProvider resolves regardless of launching cwd:
    var builder = WebApplication.CreateBuilder(new WebApplicationOptions
    {
        ContentRootPath = AppContext.BaseDirectory
    });
  • Compose the static-web-assets manifest on all target frameworks (previously net9+-only). ASP.NET Core only does this automatically in Development; the tool runs as Production:
    builder.WebHost.UseStaticWebAssets();   // now unconditional
  • Serve _framework/* + scoped CSS via the endpoints manifest on net9+:
    #if NET9_0_OR_GREATER
    app.MapStaticAssets();   // guarded: only when the manifest exists (test host has none)
    #endif
  • On .NET 8, add a second UseStaticFiles over WebRootFileProvider so manifest-mapped RCL _content/** assets (BlazorMonaco) resolve. There is no MapStaticAssets on net8, and the explicit wwwroot provider can't see the composed manifest:
    #if !NET9_0_OR_GREATER
    if (app.Environment.WebRootFileProvider is not null)
    {
        app.UseStaticFiles(new StaticFileOptions
        {
            FileProvider = app.Environment.WebRootFileProvider
        });
    }
    #endif

The existing wwwroot UseStaticFiles provider is preserved (authoritative for the installed tool, where all assets are published directly into wwwroot). For an installed tool the second net8 lookup resolves to the same wwwroot, 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), and app.css — asserting HTTP 200 with non-zero bytes across every combination of {.NET 8, .NET 10} × {Production, Development} × {dotnet run, installed tool}:

Case blazor.web.js monaco loader.js app.css Result
run / net8.0 / Production 200, 187162 200, 30061 200, 3946 ✅ PASS
run / net8.0 / Development 200, 187162 200, 30061 200, 3946 ✅ PASS
run / net10.0 / Production 200, 200575 200, 30061 200, 3946 ✅ PASS
run / net10.0 / Development 200, 200575 200, 30061 200, 3946 ✅ PASS
installed / net8.0 / Production 200, 187162 200, 30061 200, 3946 ✅ PASS
installed / net8.0 / Development 200, 187162 200, 30061 200, 3946 ✅ PASS
installed / net10.0 / Production 200, 200575 200, 30061 200, 3946 ✅ PASS
installed / net10.0 / Development 200, 200575 200, 30061 200, 3946 ✅ PASS

(Before this change: run/net8.0/* had monaco loader.js -> 404 and run/net10.0/* had blazor.web.js -> 200, 0 bytes.) Real-browser confirmation via headless Chromium: window.Blazor=true and no console errors on /, /documentation, and /durable-execution. Both TFMs build with 0 warnings / 0 errors; the Test Tool unit tests (which invoke Startup directly) pass on net8.

…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.
…esponses)

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).
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.BaseDirectory so WebRootFileProvider resolves wwwroot correctly 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.

@GarrettBeatty
GarrettBeatty marked this pull request as ready for review July 23, 2026 13:37
@GarrettBeatty
GarrettBeatty requested review from a team as code owners July 23, 2026 13:37
@GarrettBeatty
GarrettBeatty requested a review from normj July 23, 2026 13:37
@GarrettBeatty
GarrettBeatty marked this pull request as draft July 23, 2026 19:44
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).
GarrettBeatty added a commit that referenced this pull request Jul 23, 2026
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.
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