Skip to content

dotnet watch ignores StaticWebAssetPath metadata on @(Watch) items, preventing custom static file hot reload #52119

@phil-scott-78

Description

@phil-scott-78

I'm not finding a ton of documentation out there, but I've been trying to wire up dotnet watch to look at a separate folder than wwwroot that would trigger a browser refresh, but I'm stuck.

It seems the StaticWebAssetPath metadata on MSBuild @(Watch) items is silently ignored by dotnet watch. This prevents users from enabling static file hot reload (browser refresh without rebuild) for custom file types outside of wwwroot/.

Repro Steps

  1. Create an ASP.NET Core 10 project
  2. Add a markdown file outside wwwroot (e.g., Content/docs/readme.md)
  3. Add the following to the .csproj:
    <ItemGroup>
      <Watch Include="Content/**/*.md" StaticWebAssetPath="%(Identity)" />
    </ItemGroup>
  4. Run dotnet watch
  5. Edit the markdown file

Expected Behavior

The file change should trigger a browser refresh via the UpdateStaticFile WebSocket message (same as files under wwwroot/), without triggering "No C# changes to apply."

Actual Behavior

 dotnet watch ⌚ File updated: .\Content\docs\readme.md
 dotnet watch ⌚ No C# changes to apply.

The file is watched, but goes through the managed code change handler instead of the static file handler.

My understand is that the routing decision happens in https://github.com/dotnet/sdk/blob/main/src/BuiltInTools/Watch/HotReload/StaticFileHandler.cs, lines 22-26:

 if (file.StaticWebAssetPath is null)
 {
     allFilesHandled = false;  // File passes through to managed code handler
     continue;                  // Skip static file handling
 }

When StaticWebAssetPath is set:

  1. Line 28: Logs "Handling file change event for static content"
  2. Line 49: Adds file to browser refresh request list
  3. Line 64: Calls UpdateStaticAssetsAsync() → sends WebSocket UpdateStaticFile message to browser
  4. Line 68: Logs "Hot reload of static assets succeeded"

When StaticWebAssetPath is null (current behavior):

  • File is skipped (allFilesHandled = false)
  • Falls through to CompilationHandler.HandleManagedCodeChangesAsync()
  • Results in "No C# changes to apply."

Root Cause

In https://github.com/dotnet/sdk/blob/main/src/BuiltInTools/Watch/Build/EvaluationResult.cs, lines 120-127:

 var items = projectInstance.GetItems(ItemNames.Compile)
     .Concat(projectInstance.GetItems(ItemNames.AdditionalFiles))
     .Concat(projectInstance.GetItems(ItemNames.Watch));

 foreach (var item in items)
 {
     AddFile(item.EvaluatedInclude, staticWebAssetPath: null);  // <-- BUG maybe?? Always null
 }

The StaticWebAssetPath metadata is always passed as null for @(Watch) items. Compare this to the @(Content) item handling (lines 129-144), which correctly reads and passes the metadata.

Meanwhile, the MSBuild targets file https://github.com/dotnet/sdk/blob/main/src/BuiltInTools/dotnet-watch/Watch/DotNetWatch.targets (lines 70-73) does set StaticWebAssetPath on @(Watch) items for content files:

 <Watch Include="%(Content.FullPath)"
        Condition="..."
        StaticWebAssetPath="$(_DotNetWatchStaticWebAssetBasePath)$([System.String]::Copy('%(Identity)').Replace('\','/'))" />

This metadata is populated but never read.

I haven't tested this yet, but it looks like a tweak might be in EvaluationResult.cs, read the StaticWebAssetPath metadata from @(Watch items:

 var items = projectInstance.GetItems(ItemNames.Compile)
     .Concat(projectInstance.GetItems(ItemNames.AdditionalFiles))
     .Concat(projectInstance.GetItems(ItemNames.Watch));

 foreach (var item in items)
 {
     var staticWebAssetPath = item.GetMetadataValue(MetadataNames.StaticWebAssetPath);
     AddFile(item.EvaluatedInclude,
             staticWebAssetPath: string.IsNullOrEmpty(staticWebAssetPath) ? null : staticWebAssetPath);
 }

This would allow users to:

  1. Enable static file hot reload for files outside wwwroot/
  2. Mark any file type (markdown, json, xml, etc.) as a static asset
  3. Use the existing StaticWebAssetPath metadata that's already documented in the targets file

I think this is only on .NET 10. I believe some enhancements were made that prevent refresh when there are no code changes, but the scenario where we are watching stuff that is not in wwwroot, c#, CSS or wwwroot was missed.

Environment

  • .NET SDK version: 10.0.101
  • OS: Windows

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions