Skip to content

OverrideHtmlAssetPlaceholders breaks hot reload during dotnet watch in Blazor WebAssembly #53331

Description

@rsuk-mb

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

When OverrideHtmlAssetPlaceholders is set to true in a standalone Blazor WebAssembly project and index.html contains #[.{fingerprint}] placeholders (as documented for asset fingerprinting), hot reload via dotnet watch stops working. Code changes are detected and deltas are sent to the browser, but they are never applied — the page does not update.

Root cause

Setting OverrideHtmlAssetPlaceholders=true automatically sets BlazorFingerprintBlazorJs=true (in Microsoft.NET.Sdk.BlazorWebAssembly.6_0.targets, line 58). This causes blazor.webassembly.js to be fingerprinted with a content hash (e.g., blazor.webassembly.k8lfu09m0y.js).

During dotnet watch, each rebuild produces a different fingerprint hash, which changes the script filename in the processed index.html. The hot reload infrastructure (aspnetcore-browser-refresh.js) delivers deltas successfully over the WebSocket connection, but the delta application mechanism fails because the entry point script filename has changed between the initial page load and the rebuild.

The browser console shows deltas being received but never applied. No error is shown — hot reload silently fails.

Key finding from investigation

Through systematic isolation testing we confirmed:

Configuration Hot reload
OverrideHtmlAssetPlaceholders=true + #[.{fingerprint}] in index.html Broken
OverrideHtmlAssetPlaceholders=true + plain blazor.webassembly.js in index.html Works
OverrideHtmlAssetPlaceholders not set + plain blazor.webassembly.js Works

The fingerprint placeholder in the HTML combined with JS fingerprinting is the specific combination that breaks hot reload. The OverrideHtmlAssetPlaceholders property alone (without the placeholder in HTML) does not cause the issue.

To Reproduce

  1. Create a new Blazor WebAssembly Standalone app:

    dotnet new blazorwasm -o BlazorHotReloadRepro
    
  2. Add OverrideHtmlAssetPlaceholders to the .csproj:

    <PropertyGroup>
      <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
    </PropertyGroup>
  3. Update wwwroot/index.html to use the fingerprint placeholder on the Blazor script tag:

    <script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
  4. Run with dotnet watch:

    dotnet watch run
    
  5. Open the app in the browser, navigate to a page.

  6. Make a visible change to a .razor file (e.g., change text in Home.razor).

  7. Expected: The change appears in the browser via hot reload.
    Actual: The change is never applied. The browser console shows hot reload deltas received but not applied.

  8. To confirm the fix, add this to the .csproj:

    <PropertyGroup>
      <BlazorFingerprintBlazorJs Condition="'$(DOTNET_WATCH)' == '1'">false</BlazorFingerprintBlazorJs>
    </PropertyGroup>

    Restart dotnet watch — hot reload now works.

Suggested fix

The SDK should automatically set BlazorFingerprintBlazorJs=false when DOTNET_WATCH=1. This is consistent with how other watch-specific optimizations are already handled (e.g., the SDK disables documentation generation during watch builds).

In Microsoft.NET.Sdk.BlazorWebAssembly.6_0.targets, the existing line:

<BlazorFingerprintBlazorJs Condition="'$(BlazorFingerprintBlazorJs)' == '' and '$(OverrideHtmlAssetPlaceholders)' == 'true'">true</BlazorFingerprintBlazorJs>

Could be updated to:

<BlazorFingerprintBlazorJs Condition="'$(BlazorFingerprintBlazorJs)' == '' and '$(OverrideHtmlAssetPlaceholders)' == 'true' and '$(DOTNET_WATCH)' != '1'">true</BlazorFingerprintBlazorJs>

The OverrideHtmlAssetPlaceholders task with IncludeOnlyHardFingerprintedModules=false (used for build) correctly resolves #[.{fingerprint}] to an empty string when the asset isn't fingerprinted, so the HTML output is correct with just this property change.

Workaround

Add to Directory.Build.props (or individual .csproj):

<PropertyGroup>
  <BlazorFingerprintBlazorJs Condition="'$(Configuration)' == 'Debug'">false</BlazorFingerprintBlazorJs>
</PropertyGroup>

This disables JS fingerprinting in Debug builds, preserving hot reload for both dotnet watch and Visual Studio. Release/Publish builds retain full fingerprinting.

Exceptions (if any)

No exceptions thrown. Hot reload silently fails — deltas are received but not applied.

.NET Version

10.0.100-preview.5 (also reproducible on 10.0.200-preview.0.26103.119)

Anything else?

  • The OverrideHtmlAssetPlaceholders task itself works correctly — when BlazorFingerprintBlazorJs=false, it resolves #[.{fingerprint}] to empty string, producing clean blazor.webassembly.js references.
  • The issue also affects Visual Studio hot reload, not just dotnet watch.
  • Stale build artifacts can compound the issue — cleaning the artifacts directory is sometimes needed after toggling the property.

Metadata

Metadata

Assignees

Labels

Area-AspNetCoreRazorSDK, BlazorWebAssemblySDK, StaticWebAssetsSDKArea-Watchdotnet-watch

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions