Handle duplicate static web asset identities gracefully#53305
Handle duplicate static web asset identities gracefully#53305ilonatommy wants to merge 4 commits intodotnet:mainfrom
Conversation
Multiple StaticWebAssets MSBuild tasks crash with ArgumentException when candidate assets contain duplicate Identity keys. This happens when: - Hosted Blazor WASM projects reference multiple WASM client projects, causing dotnet.js.map from microsoft.netcore.app.runtime.mono.browser-wasm to appear multiple times via different dependency paths. - A Blazor Web App project references another Blazor Web App project, causing blazor.web.js from Microsoft.AspNetCore.App.Internal.Assets to appear twice. Fix all four code paths that build Identity-keyed dictionaries: - DiscoverPrecompressedAssets.Execute(): ToDictionary → ContainsKey loop - StaticWebAsset.ToAssetDictionary(): .Add → ContainsKey guard - GenerateStaticWebAssetsManifest: ToDictionary → ContainsKey loop - GenerateStaticWebAssetEndpointsManifest: ToDictionary → ContainsKey loop In all cases, the first occurrence is kept and subsequent duplicates are silently skipped. DiscoverPrecompressedAssets additionally logs skipped duplicates at Low message importance for diagnostics. Note: For the Blazor-references-Blazor scenario (dotnet#52089), this fix converts the unhandled ArgumentException crash into a proper diagnostic error (Conflicting assets with the same target path). The architectural fix for that scenario is tracked in dotnet#53135 (Framework SourceType), which prevents framework asset duplicates from being generated in the first place. Fixes dotnet#52089 Fixes dotnet#52647 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Thanks for your PR, @@ilonatommy. |
There was a problem hiding this comment.
Pull request overview
This PR prevents Static Web Assets MSBuild tasks from crashing when static web asset item groups contain duplicate Identity values, by changing Identity-keyed dictionary construction to skip subsequent duplicates.
Changes:
- Replaced
ToDictionary(...)calls withDictionary+ duplicate-guard loops in manifest generation tasks. - Updated
StaticWebAsset.ToAssetDictionary()to useOSPath.PathComparerand skip duplicate identities instead of throwing. - Updated
DiscoverPrecompressedAssetsto dedupe candidate assets by identity and emit a low-importance diagnostic when duplicates are skipped.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsManifest.cs | Avoids ToDictionary duplicate-key crashes when filtering publish endpoints. |
| src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetEndpointsManifest.cs | Avoids ToDictionary duplicate-key crashes when building the endpoints manifest asset map. |
| src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs | Makes ToAssetDictionary() resilient to duplicate identities (and uses OS-specific path comparer). |
| src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs | Dedupes candidates by identity and logs when duplicates are skipped to prevent task failure. |
- Remove OSPath.PathComparer from ToAssetDictionary since StaticWebAsset.cs
is shared into BlazorWasmSdk which doesn't include OSPath.cs, causing
CS0103 build errors. The original code used the default comparer, so this
restores that behavior while keeping the duplicate-handling fix.
- Add DuplicateAssetIdentityHandlingTest with 6 targeted tests covering:
- ToAssetDictionary: first occurrence wins, no ArgumentException
- DiscoverPrecompressedAssets: duplicate candidates handled, compressed
pairs still discovered
- GenerateStaticWebAssetsManifest (Publish): no crash with duplicates
- GenerateStaticWebAssetEndpointsManifest: no crash with duplicates
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address review feedback: all four duplicate-handling code paths now log at MessageImportance.Low when a duplicate identity is skipped, including the SourceId of both the kept and discarded assets for diagnostics. - ToAssetDictionary: accepts optional TaskLoggingHelper parameter; callers can pass Log to enable diagnostics without breaking existing signatures - GenerateStaticWebAssetsManifest: logs skipped duplicates in publish endpoint filtering - GenerateStaticWebAssetEndpointsManifest: logs skipped duplicates in manifest asset collection - DiscoverPrecompressedAssets: already had logging (unchanged) Also switched ContainsKey+indexer patterns to TryGetValue to satisfy CA1854 analyzer rule (avoid double dictionary lookup). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When duplicate asset identities have differing metadata (SourceId, RelativePath, or TargetPath), emit a build warning instead of a low-importance message. Identical duplicates (same file via multiple dependency paths) remain at MessageImportance.Low since they are harmless and expected in multi-WASM-project scenarios. This distinction helps surface cases where deduplication silently loses meaningful data (e.g., same file mapped to different target paths) while keeping the noise down for the common identical case. Updated tests to cover both paths: - Identical duplicates → Low message, no warning - Mismatched duplicates → Warning with metadata details Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Hey @ilonatommy, thanks for the thorough investigation here — the repro work and root cause analysis are really clear. I wanted to share some context on this since I'm the one who introduced the regression (runtime#124125). Here's the chain of events:
The root cause is specifically that Why I think the ContainsKey approach is risky here: Javier's SWA pipeline deliberately uses The fix I'm working on is in runtime's
I have the fix applied in a local VMR build that's running now and should have a runtime PR up soon. cc @javiercn |
I agree, I don't like the part of dropping one of the entries either. As soon as your PR is out, feel free to close this one. |
|
Closing in favor of dotnet/runtime#125309. |
|
Following up on my earlier comment — after deeper investigation, I've confirmed the SDK-only approach in this PR is the correct fix. The runtime copy approach I was originally pursuing (dotnet/runtime#125309) re-introduces the staleness problem that runtime#124125 was specifically fixing. The duplicate Identity scenario is valid: Identity = FullPath, so same Identity = same physical file on disk (shared NuGet cache). Skip-duplicate is always semantically correct here. I independently arrived at the same fix (same 4 files, same pattern) and validated it end-to-end: build ✅, publish ✅, multi-client WASM ✅. Your version with One note: |
|
Spoke offline with @lewing. This is not the right approach to solve this problem. The issue stems from a target (in our case wasm, but blazor web does the same) introducing multiple definitions of the same asset, for example, dotnet.js or blazor.web.js. this typically happens through project references (multiple wasm apps, multiple "blazor web apps", although this is not a thing) and fails on the consuming project because unlike for packages which naturally only include a single version. For assets introduced this way, they get defined in the context of the consuming project. The results is that the two duplicates have the same identity (which is not expected), but end up with different metadata properties (legitimately). Choosing a winner is not the right approach as it will inevitably break one or another app. For example, if you pick a winner for dotnet.js, you could have Assets being unique across the entire pipeline (that is their Identity being unique across the entire pipeline) has been a constraint / design invariant for years, and not something we should change lightly. |
|
dotnet/runtime#125329 is where I'm prototyping things now |
Summary
Multiple
StaticWebAssetsMSBuild tasks crash withArgumentException: An item with the same key has already been addedwhen candidate assets contain duplicateIdentitykeys. This PR handles duplicates gracefully by keeping the first occurrence and skipping subsequent ones.What causes duplicates
Two known scenarios produce duplicate asset identities:
Hosted Blazor WASM projects referencing multiple WASM client projects —
dotnet.js.mapfrommicrosoft.netcore.app.runtime.mono.browser-wasmappears multiple times via different dependency paths. This blocks thedotnet/aspnetcoreCI codeflow PR (aspnetcore#65673).Blazor Web App referencing another Blazor Web App —
blazor.web.jsfromMicrosoft.AspNetCore.App.Internal.Assetsappears twice (repro: DanielSundberg/DiscoverPrecompressedAssets).What this PR fixes
All four code paths that build Identity-keyed dictionaries from static web asset items:
DiscoverPrecompressedAssets.csToDictionary→ContainsKeyloop with diagnostic logStaticWebAsset.ToAssetDictionary().Add→ContainsKeyguard (used by 5 other tasks)GenerateStaticWebAssetsManifest.csToDictionary→ContainsKeyloopGenerateStaticWebAssetEndpointsManifest.csToDictionary→ContainsKeyloopVerified locally
dotnet/aspnetcorecodeflow branch (darc-main-b867beb1-...) with SDK11.0.100-preview.3.26128.104HostedInAspNet.Serverbuilds successfully after patchingBlazorAppreferencingBlazorPlugin)DiscoverPrecompressedAssetscrash is eliminatedWhat this does NOT fix
For the Blazor-references-Blazor scenario (#52089), eliminating the
ArgumentExceptioncrash surfaces a different, pre-existing error:Conflicting assets with the same target path '_framework/blazor.server.js'This conflict occurs because the same framework asset arrives from two projects with different
SourceType(DiscoveredvsProject). The proper architectural fix for this is tracked in #53135 (FrameworkSourceType by @javiercn), which prevents framework asset duplicates from being generated in the first place.This PR and #53135 are complementary:
ToDictionarycrashes regardless of duplication sourceFixes #52089
Fixes #52647