dotnet / dotnet/sdk

Static web assets discovery only sees wwwroot content present at project evaluation time, not files written by a BeforeBuild/Build-hooked target

Open
#55,883 5 comments 1 reaction 0 assignees View on GitHub
Area-AspNetCore untriaged
Dominant language
C#
Stars
3.2k
Forks
1.3k
PR merge metrics
PR metrics pending

Description

### Summary

A custom MSBuild target hooked at `BeforeTargets="BeforeBuild"` (or `"Build"`) that writes a generated file straight into `wwwroot` — the common pattern taught by several real "add Tailwind/Sass/esbuild to ASP.NET Core" tutorials — can silently fail to get a route in the published static web assets endpoints manifest (`{Project}.staticwebassets.endpoints.json`), even though the file itself is written to disk correctly at the right path and size. Under `MapStaticAssets()` (added in .NET 9), that means the request 404s in production despite the asset existing.

Originally filed against `dotnet/aspnetcore` — full investigation, multiple real repro repos, and CI runs are there: https://github.com/dotnet/aspnetcore/issues/68641. Filing here too since tracing it into the actual source landed in this repo, not aspnetcore.

### Root cause (as far as traced)

`ResolveProjectStaticWebAssets` is the target responsible for turning `wwwroot` content into `StaticWebAsset`/manifest entries:

https://github.com/dotnet/sdk/blob/7053edd97335c939e3d7692b4aea58e7c9c2bc6d/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets#L768-L812

```xml

...
`** inside the generating target — the pattern from Microsoft's own [Razor Class Library client assets guidance](https://devblogs.microsoft.com/dotnet/build-client-web-assets-for-your-razor-class-library/). This is the fix that's held up in every test.

### Minimal repro

https://github.com/Popl7/essessay-tailwind-manifest-repro — several branches isolating variables one at a time:
- `main` — original Tailwind CLI, `BeforeTargets="BeforeBuild"`
- `no-external-process-repro` — same bug with a trivial in-process `WriteLinesToFile`, no Tailwind, no external process at all
- `tutorial-pattern-repro` — the literal shape real tutorials teach (`BeforeTargets="Build"`)
- `dotnet8-repro` / `dotnet9-repro` / `dotnet11-repro` — same underlying discovery gap exists on .NET 8 too (confirmed via `obj/**/staticwebassets.build.json`), but is only user-visible from .NET 9 onward since `UseStaticFiles()` (pre-`MapStaticAssets()`) never consults that manifest for a plain app's own assets
- `official-pattern-repro` — the `Content`/`Link` fix, confirmed working

Each has a GitHub Actions workflow (`workflow_dispatch`) that builds with `--no-cache` and checks both the manifest and the actual HTTP response.

### What's still unexplained

The committed-vs-gitignored mechanism above is fully confirmed. What I haven't traced into the source: **from-scratch minimal repros with no pre-existing file on either side reproduce 100% reliably on GitHub Actions/Render, but never reproduce on local Docker Desktop (macOS, arm64 and amd64-emulated, under various CPU/memory constraints)** — same commit, same Dockerfile, same single `dotnet publish` invocation. Given `Content` is fixed at evaluation time either way, I'd naively expect this to be fully deterministic (always missing) once there's genuinely no pre-existing file — but it isn't, it's environment-dependent. Something after this point (possibly in Publish's own separate resolution pass, or an evaluation-cache detail) must be doing something I haven't found. Flagging in case it's a useful thread for someone closer to this code.

Happy to provide binlogs from a passing and a failing build if that would help narrow it down.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets, especially ResolveProjectStaticWebAssets and its CandidateAssets input. Run the minimal repro branches and their GitHub Actions workflows, comparing the generated static web assets manifest with the HTTP response. Use passing and failing binlogs to investigate the remaining environment-dependent behavior; done means the discovery behavior and expected manifest result are explained and covered by a regression test.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, github-actions
Domain
build-system, devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.