Skip to content

Add Framework SourceType for static web assets#53135

Merged
javiercn merged 9 commits intomainfrom
javiercn/framework-assets
Mar 9, 2026
Merged

Add Framework SourceType for static web assets#53135
javiercn merged 9 commits intomainfrom
javiercn/framework-assets

Conversation

@javiercn
Copy link
Copy Markdown
Member

@javiercn javiercn commented Feb 24, 2026

Add Framework SourceType for Static Web Assets

Summary

This PR adds a new Framework source type to the Static Web Assets (SWA) pipeline. Assets marked as Framework in a NuGet package are "adopted" by each consuming library: copied from the package cache to a project-local intermediate directory and transformed so the pipeline treats them as local Discovered assets owned by the consuming project.

The feature has two halves:

  1. Pack side: GenerateStaticWebAssetsPropsFile evaluates a $(StaticWebAssetFrameworkPattern) glob against each asset's RelativePath and emits matching assets with SourceType="Framework" instead of the default "Package".
  2. Consume side: UpdatePackageStaticWebAssets identifies Framework assets, materializes them to a project-local path, transforms their metadata to match the consuming project, and remaps associated endpoints.

Motivation

When multiple Root-mode libraries reference the same NuGet package containing static web assets, the consuming project receives duplicate asset definitions with different SourceId values — one per referencing library. The pipeline treats these as conflicting claims to the same RelativePath, producing "Duplicate Asset" build errors even though they point to the same physical file.

The primary scenario is Blazor's blazor.web.js — multiple libraries depend on a common package (Assets.Internal) containing the same framework file. Without this feature, the consuming project fails with a conflict because it receives definitions for the same file via multiple library references with different SourceIds.

Goals

  • Define and implement the "Framework" asset concept. Support a category of static web assets that originate from NuGet packages but are materialized into the consuming project's intermediate output, effectively becoming local artifacts during the build.
  • Resolve conflicts for shared framework dependencies. Prevent build failures when multiple libraries depend on the same framework assets by copying them to a canonical location and unifying their definition.
  • Transform asset identity and ownership. Modify metadata of Framework assets to reassign ownership to the consuming project (SourceType→Discovered, SourceId→$(PackageId), BasePath→$(StaticWebAssetBasePath), AssetMode→CurrentProject).
  • Enable packaging and distribution via NuGet. Allow library authors to mark assets as Framework using $(StaticWebAssetFrameworkPattern) glob at pack time.

Design Overview

The design introduces a new Framework source type that flows through two distinct pipeline stages — pack and consume — touching three existing primitives with no new tasks or targets required.

Component Diagram

graph TD
    subgraph "Pack Time (Library)"
        A["ComputeReferenceStaticWebAssetItems"] --> B["GenerateStaticWebAssetsPropsFile"]
        FP["$(StaticWebAssetFrameworkPattern)"] -.-> B
        B --> |"SourceType=Framework for glob matches"| C[".nupkg props file"]
    end

    subgraph "Consume Time (Consumer Project)"
        D["NuGet restore imports .props"] --> E["StaticWebAsset items<br/>(SourceType=Framework)"]
        E --> F["UpdatePackageStaticWebAssets"]
        F --> |"Materialize + Transform"| G["Adopted assets<br/>(SourceType=Discovered,<br/>AssetMode=CurrentProject)"]
        F --> |"Remap AssetFile"| H["Updated StaticWebAssetEndpoints"]
        G --> I["Rest of SWA pipeline"]
        H --> I
    end

    subgraph "StaticWebAsset.cs"
        J["SourceTypes.Framework"]
        K["IsFramework()"]
        L["Validate() accepts Framework"]
        M["ComputeTargetPath suppresses BasePath"]
    end

    C -.-> D
    J -.-> F
    K -.-> F
Loading

Modified Primitives

StaticWebAsset (Tasks/Data/StaticWebAsset.cs)

Register Framework as a valid source type. Add type-check helpers. Update path computation to suppress BasePath for Framework assets (same behavior as Discovered/Computed). Update validation to accept Framework.

  public static class SourceTypes
  {
      public const string Discovered = nameof(Discovered);
      public const string Computed = nameof(Computed);
      public const string Project = nameof(Project);
      public const string Package = nameof(Package);
+     public const string Framework = nameof(Framework);

      public static bool IsPackage(string sourceType);
+     public static bool IsFramework(string sourceType);
  }
  • Validate(): Adds case SourceTypes.Framework: break; arm.
  • ComputeTargetPath() / CreatePathString(): Condition that suppresses BasePath changes from IsDiscovered() || IsComputed() to IsDiscovered() || IsComputed() || IsFramework().

UpdatePackageStaticWebAssets (Tasks/UpdatePackageStaticWebAssets.cs)

Extend the existing task to identify, materialize, and transform Framework assets. Add endpoint remapping.

  public class UpdatePackageStaticWebAssets : Task
  {
      [Required]
      public ITaskItem[] Assets { get; set; }

+     public string IntermediateOutputPath { get; set; }
+     public string ProjectPackageId { get; set; }
+     public string ProjectBasePath { get; set; }
+     public ITaskItem[] Endpoints { get; set; }

      [Output]
      public ITaskItem[] UpdatedAssets { get; set; }
      [Output]
      public ITaskItem[] OriginalAssets { get; set; }

+     [Output]
+     public ITaskItem[] RemappedEndpoints { get; set; }
+     [Output]
+     public ITaskItem[] OriginalRemappedEndpoints { get; set; }
  }

For each Framework asset, Execute():

  1. Materializes the file from package cache to {IntermediateOutputPath}fx/{OriginalSourceId}/{RelativePath} with timestamp-based incremental copy.
  2. Transforms metadata: Identity → materialized path, SourceTypeDiscovered, SourceId$(PackageId), BasePath$(StaticWebAssetBasePath), AssetModeCurrentProject.
  3. Remaps endpoints: Groups endpoints by route (Identity), updates AssetFile for any endpoint referencing a materialized asset's old identity (group-based to handle MSBuild's Remove-by-ItemSpec semantics).

GenerateStaticWebAssetsPropsFile (Tasks/GenerateStaticWebAssetsPropsFile.cs)

Add FrameworkPattern input. Evaluate each asset's RelativePath against the glob using StaticWebAssetGlobMatcher and emit matching assets with SourceType="Framework" in the output XML.

  public class GenerateStaticWebAssetsPropsFile : Task
  {
+     public string FrameworkPattern { get; set; }
  }

Target Changes

UpdateExistingPackageStaticWebAssets (Microsoft.NET.Sdk.StaticWebAssets.targets): Pass new parameters, capture endpoint remapping outputs, apply remove/add for remapped endpoints.

  <UpdatePackageStaticWebAssets
-   Assets="@(StaticWebAsset)">
+   Assets="@(StaticWebAsset)"
+   Endpoints="@(StaticWebAssetEndpoint)"
+   IntermediateOutputPath="$(_StaticWebAssetsIntermediateOutputPath)"
+   ProjectPackageId="$(PackageId)"
+   ProjectBasePath="$(StaticWebAssetBasePath)">
    <Output TaskParameter="UpdatedAssets" ItemName="_UpdatedPackageAssets" />
    <Output TaskParameter="OriginalAssets" ItemName="_OriginalPackageAssets" />
+   <Output TaskParameter="RemappedEndpoints" ItemName="_RemappedFrameworkEndpoints" />
+   <Output TaskParameter="OriginalRemappedEndpoints" ItemName="_OriginalRemappedEndpoints" />
  </UpdatePackageStaticWebAssets>

GenerateStaticWebAssetsPackFiles (Microsoft.NET.Sdk.StaticWebAssets.Pack.targets): Pass $(StaticWebAssetFrameworkPattern) to GenerateStaticWebAssetsPropsFile.

  <GenerateStaticWebAssetsPropsFile
    StaticWebAssets="@(_PackStaticWebAssets)"
-   TargetPropsFilePath="$(_GeneratedStaticWebAssetsPropsFile)" />
+   TargetPropsFilePath="$(_GeneratedStaticWebAssetsPropsFile)"
+   FrameworkPattern="$(StaticWebAssetFrameworkPattern)" />

Scenarios

Scenario 1: Packaging Framework Assets

A library author sets <StaticWebAssetFrameworkPattern>**/*.js</StaticWebAssetFrameworkPattern> in its project file. During GenerateStaticWebAssetsPackFiles, GenerateStaticWebAssetsPropsFile receives the glob via FrameworkPattern, builds a StaticWebAssetGlobMatcher, and evaluates each asset's RelativePath. Matching .js files are emitted with SourceType="Framework" in the generated props file; non-matching files retain SourceType="Package". The physical package layout is unchanged.

Scenario 2: Consuming Framework Assets

After NuGet restore, the package's .props chain defines StaticWebAsset items with SourceType="Framework". When UpdateExistingPackageStaticWebAssets runs (first in ResolveCoreStaticWebAssets), UpdatePackageStaticWebAssets materializes each Framework asset to $(_StaticWebAssetsIntermediateOutputPath)fx/{OriginalSourceId}/{RelativePath}, transforms metadata (SourceType→Discovered, SourceId→$(PackageId), BasePath→$(StaticWebAssetBasePath), AssetMode→CurrentProject), and remaps associated endpoints. Downstream targets see ordinary local Discovered assets.

Scenario 3: Shared Framework Dependency

Two Root-mode libraries (LibraryA, LibraryB) both reference the same package (Assets.Internal) containing Framework assets. Each library independently materializes the framework files and transforms them with their own SourceId and BasePath. Both libraries set distinct StaticWebAssetBasePath values, so the materialized assets resolve to different target paths (_content/LibraryA/js/framework.js and _content/LibraryB/js/framework.js). When a consuming project receives these via project references, no duplicate asset error occurs because the paths do not collide.


Key Design Decisions

Decision Choice Rationale
Extend existing task vs. new task Extend UpdatePackageStaticWebAssets Both Package and Framework assets originate from NuGet. Processing together avoids second iteration, keeps target chain unchanged.
Transformed SourceType Discovered Most accurate characterization — materialized file is a real file on disk. Avoids downstream changes.
AssetMode after transformation CurrentProject In Root mode, CurrentProject assets flow through references. In Default mode, prevents leaking to references — each project materializes its own copy.
Glob classification point At GenerateStaticWebAssetsPropsFile Framework/Package is an output-only concern. Input assets stay Discovered/Computed. Mirrors how Package works today.
Endpoint remapping strategy Grouped by route Identity MSBuild Remove operates on ItemSpec (route). Grouping ensures all endpoints in a route group are processed together, preventing orphans.
Fingerprint/integrity handling Preserve from FromV1TaskItem File.Copy produces byte-identical content. Recomputing adds unnecessary I/O.

Error Handling

No custom error handling for framework asset resolution. Standard I/O exceptions propagate if source files don't exist. Two packages cannot conflict at the same materialization path because the original package ID is included (fx/{SourceId}/{RelativePath}). Existing SWA duplicate asset detection handles same-target-path conflicts. Zero glob matches is a no-op.

Testing

  • Unit tests for GenerateStaticWebAssetsPropsFile glob matching (Framework vs. Package classification)
  • Unit tests for UpdatePackageStaticWebAssets framework asset materialization and metadata transformation
  • Unit tests for endpoint remapping (single endpoint, multiple endpoints per route, mixed Framework/Package)
  • Integration tests for shared framework dependency scenario (two Root-mode libraries, same package)
  • Integration tests for packaging and consuming framework assets end-to-end

@github-actions github-actions bot added the Area-AspNetCore RazorSDK, BlazorWebAssemblySDK, StaticWebAssetsSDK label Feb 24, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Thanks for your PR, @@javiercn.
To learn about the PR process and branching schedule of this repo, please take a look at the SDK PR Guide.

@javiercn javiercn marked this pull request as ready for review February 24, 2026 18:57
Copilot AI review requested due to automatic review settings February 24, 2026 18:57
@javiercn javiercn force-pushed the javiercn/framework-assets branch from bf32e7b to 7b30101 Compare February 24, 2026 18:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new Framework SourceType to the Static Web Assets (SWA) pipeline so selected package assets can be “adopted” (materialized into a project-local intermediate location and transformed to behave like local discovered assets), reducing duplicate-asset conflicts when shared framework files flow through multiple references.

Changes:

  • Pack-time: GenerateStaticWebAssetsPropsFile classifies assets matching $(StaticWebAssetFrameworkPattern) as SourceType=Framework in generated props.
  • Consume-time: UpdatePackageStaticWebAssets materializes Framework assets into obj/.../staticwebassets/fx/..., transforms metadata to Discovered/CurrentProject, and remaps endpoints.
  • Pipeline support: StaticWebAsset recognizes Framework as a valid source type and updates target-path logic; targets/tests updated to exercise end-to-end behavior.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
test/TestAssets/TestProjects/FrameworkAssetsSample/FrameworkAssetsLib/wwwroot/js/framework.js Adds sample JS asset intended to be packed as Framework.
test/TestAssets/TestProjects/FrameworkAssetsSample/FrameworkAssetsLib/wwwroot/css/site.css Adds sample CSS asset intended to remain Package.
test/TestAssets/TestProjects/FrameworkAssetsSample/FrameworkAssetsLib/FrameworkAssetsLib.csproj New test library that sets StaticWebAssetFrameworkPattern.
test/TestAssets/TestProjects/FrameworkAssetsSample/FrameworkAssetsConsumer/Program.cs Minimal consumer app entry point for integration scenario.
test/TestAssets/TestProjects/FrameworkAssetsSample/FrameworkAssetsConsumer/FrameworkAssetsConsumer.csproj Consumer project that references packed library and validates isolation setup.
test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdatePackageStaticWebAssetsTest.cs New unit tests for framework-asset materialization + endpoint remap behavior.
test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsPropsFileTest.cs Adds tests for pack-time FrameworkPattern classification.
test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsIntegrationTest.cs New integration tests covering pack + consume + incrementality.
src/StaticWebAssetsSdk/Tasks/UpdatePackageStaticWebAssets.cs Implements framework-asset materialization, metadata transform, and endpoint remapping.
src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsPropsFile.cs Adds FrameworkPattern matching to emit SourceType=Framework.
src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs Registers Framework source type and updates validation/path computation.
src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets Wires new task parameters + endpoint remove/add remapping into SWA targets.
src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Pack.targets Passes $(StaticWebAssetFrameworkPattern) into pack props generation.

Copy link
Copy Markdown
Member

@maraf maraf left a comment

Choose a reason for hiding this comment

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

Looks good to me & my copilot 👍

ilonatommy added a commit to ilonatommy/sdk that referenced this pull request Mar 6, 2026
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 <[email protected]>
lewing added a commit to lewing/runtime that referenced this pull request Mar 9, 2026
… solutions

When multiple Blazor WebAssembly client projects reference the same runtime pack,
pass-through files (JS, maps, ICU data, native wasm) share a NuGet cache path.
This causes duplicate Identity keys in the static web assets pipeline, crashing
DiscoverPrecompressedAssets with an ArgumentException.

Instead of copying pass-throughs to the intermediate output path (which risks staleness
on incremental builds when the runtime pack changes), materialize them to a per-project
obj/fx/{PackageId}/ directory using the Framework SourceType convention from the SWA SDK.
This gives each project a unique Identity while properly modeling the relationship:
these are framework assets adopted by each consuming project.

- Pass-through files: copied to obj/fx/{PackageId}/, registered with SourceType=Framework
- WebCil-converted files: remain in obj/webcil/, registered with SourceType=Computed
- Satellite assemblies: placed in culture subdirectories in both cases
- Both groups get per-item ContentRoot for correct Identity resolution

Requires SDK support for SourceType=Framework (dotnet/sdk#53135).

Fixes duplicate-key crash introduced by dotnet#124125.

Co-authored-by: Copilot <[email protected]>
lewing added a commit to lewing/runtime that referenced this pull request Mar 9, 2026
… solutions

When multiple Blazor WebAssembly client projects reference the same runtime pack,
pass-through files (JS, maps, ICU data, native wasm) share a NuGet cache path.
This causes duplicate Identity keys in the static web assets pipeline, crashing
DiscoverPrecompressedAssets with an ArgumentException.

Instead of copying pass-throughs to the intermediate output path (which risks staleness
on incremental builds when the runtime pack changes), materialize them to a per-project
obj/fx/{PackageId}/ directory using the Framework SourceType convention from the SWA SDK.
This gives each project a unique Identity while properly modeling the relationship:
these are framework assets adopted by each consuming project.

- Pass-through files: copied to obj/fx/{PackageId}/, registered with SourceType=Framework
- WebCil-converted files: remain in obj/webcil/, registered with SourceType=Computed
- Satellite assemblies: placed in culture subdirectories in both cases
- Both groups get per-item ContentRoot for correct Identity resolution

Requires SDK support for SourceType=Framework (dotnet/sdk#53135).

Fixes duplicate-key crash introduced by dotnet#124125.

Co-authored-by: Copilot <[email protected]>
@lewing
Copy link
Copy Markdown
Member

lewing commented Mar 9, 2026

I've created dotnet/runtime#125329 which depends on this PR. It uses the Framework SourceType convention to fix the duplicate Identity crash in multi-WASM-client solutions (caused by dotnet/runtime#124125).

What the runtime PR does: In Browser.targets, pass-through files (JS, maps, ICU, native wasm) are materialized to obj/fx/{PackageId}/ and registered with SourceType="Framework" via a second DefineStaticWebAssets call. WebCil-converted files remain SourceType="Computed". This gives each project unique Identity values while following the Framework materialization pattern.

Testing: Validated build + publish with multi-client projects, WebCil disabled, incremental rebuild, and aspnetcore Components.TestServer (3 WASM clients) — all passing.

The runtime change is minimal (single file, ~50 lines added to Browser.targets) and only needs the Framework SourceType to be recognized by the SDK tasks.

lewing added a commit to lewing/runtime that referenced this pull request Mar 9, 2026
… solutions

Multi-WASM-client solutions crash with duplicate Identity keys in
DiscoverPrecompressedAssets because pass-through files (JS, maps, ICU,
native wasm) share the same NuGet cache path across projects.

Instead of manually copying pass-throughs, use the SWA Framework
SourceType convention: DefineStaticWebAssets registers them as
SourceType="Framework", then UpdatePackageStaticWebAssets materializes
them to per-project obj/fx/{SourceId}/ directories, transforming
metadata (SourceType→Discovered, SourceId→PackageId,
AssetMode→CurrentProject) and giving each project a unique Identity.

This approach:
- Eliminates manual Copy tasks from Browser.targets
- Leverages the SDK's MaterializeFrameworkAsset for timestamp-based
  incremental copies
- Properly models the relationship: framework assets adopted by each
  consuming project
- Works with and without WebCil enabled

Depends on dotnet/sdk#53135 for Framework SourceType support in the SDK.

Co-authored-by: Copilot <[email protected]>
javiercn added 6 commits March 9, 2026 15:24
- Fix CreatePathString to include IsFramework() for consistent path computation
- Remove MaterializedFrameworkAssets output (unused)
- Change MaterializeFrameworkAsset return type to (StaticWebAsset, string)
- Replace dead File.Copy throw with Log.LogError
- Remove fingerprint/integrity/LastWriteTime recomputation (content identical)
- Simplify ContentRoot to EnsureTrailingSlash(fxDir)
- Fix EnsureTrailingSlash to handle both separator chars
- Rework endpoint remapping to group by Identity (MSBuild Remove semantics)
- Add Inputs/Outputs to UpdateExistingPackageStaticWebAssets target
- Clean up restating comments in GenerateStaticWebAssetsPropsFile
The task already handles copy incrementality via timestamp comparison
(File.GetLastWriteTimeUtc). Target-level Inputs/Outputs is problematic
because empty Inputs (no Framework assets) causes MSBuild to skip the
entire target, which also handles Package assets. This matches the
pattern used by other tasks in the framework (e.g.,
GenerateStaticWebAssetsDevelopmentManifest,
GenerateStaticWebAssetEndpointsManifest).
- Move Endpoints != null && assetMapping.Count > 0 guard to call site
  to avoid allocations when no framework assets exist
- Remove TaskItem clone in remapping loop; mutate AssetFile in-place
- Still two passes over each group (detect then update) since we need
  to know if the group needs remapping before modifying any items
Mutate AssetFile as we iterate, then AddRange the group to output
lists only if any endpoint was remapped.
Test project pair for validating the Framework Assets feature:
- FrameworkAssetsLib: RCL with StaticWebAssetFrameworkPattern=**/*.js
- FrameworkAssetsConsumer: Web app consuming the lib as a PackageReference
javiercn added 3 commits March 9, 2026 15:24
- UpdatePackageStaticWebAssetsTest: 16 unit tests covering
  - Package asset pass-through
  - Framework asset materialization (file copy, metadata mutation)
  - SourceType changed to Discovered, SourceId/BasePath/AssetMode updated
  - ContentRoot pointing to fx/ directory
  - Error handling for missing source files
  - Mixed asset processing (Package + Framework)
  - Fingerprint/Integrity preservation from original file
  - Incremental copy (skip when up-to-date, overwrite when stale)
  - Endpoint remapping (single, multi-identity, non-matching, null)
  - Subdirectory preservation in materialized paths

- GenerateStaticWebAssetsPropsFileTest: 4 new tests for FrameworkPattern
  - SourceType=Framework emitted when pattern matches
  - All Package when pattern is null
  - Multiple semicolon-separated patterns
  - All Package when pattern matches nothing

- FrameworkAssetsIntegrationTest: 6 integration tests
  - Pack produces nupkg with Framework SourceType in .props
  - Pack includes expected static web assets
  - Consumer build materializes framework assets to fx/ directory
  - Materialized file exists on disk
  - Endpoints remapped to materialized paths
  - Incremental build skips re-copy

- Updated test assets for test infrastructure compatibility
  (AspNetTestTfm, AspNetTestPackageSource, EnsurePackagesExist target)
…ram.cs

- Fix consumer Program.cs to use traditional Main() instead of top-level
  statements that depend on implicit usings unavailable in test env
- Fix integration test assertions to look under staticwebassets/fx/
  instead of fx/ (IntermediateOutputPath includes staticwebassets subdir)
- Fix endpoint assertion to handle compressed endpoint variants
…omparer, Ordinal for routes

- Remove OriginalRemappedEndpoints output; single RemappedEndpoints with cloned TaskItems
- Use StaticWebAsset.NormalizeContentRootPath + asset.Normalize() instead of custom EnsureTrailingSlash
- Switch assetMapping dictionary to OSPath.PathComparer for path keys
- Use StringComparer.Ordinal for endpoint route grouping (matches RouteAndAssetComparer)
@javiercn javiercn force-pushed the javiercn/framework-assets branch from e0624be to f2a0c0b Compare March 9, 2026 14:24
@javiercn javiercn merged commit b6ddae7 into main Mar 9, 2026
26 checks passed
@javiercn javiercn deleted the javiercn/framework-assets branch March 9, 2026 15:46
lewing added a commit to dotnet/runtime that referenced this pull request Mar 25, 2026
… solutions (#125329)

## Summary

When multiple Blazor WebAssembly client projects reference the same
runtime pack, pass-through files (JS, maps, ICU data, native wasm) share
a NuGet cache path. This causes duplicate Identity keys in the static
web assets pipeline, crashing `DiscoverPrecompressedAssets` with an
`ArgumentException` on `ToDictionary`.

## Root Cause

PR #124125 correctly changed WASM `ContentRoot` from project-specific
`OutputPath` copies to per-item `%(RootDir)%(Directory)` (pointing to
the real file in the NuGet cache). This fixed staleness on incremental
builds. However, when two WASM client projects reference the same
runtime pack, shared files like `dotnet.js.map` now resolve to identical
NuGet cache paths, producing duplicate Identity values.

## Fix

Use the SWA **Framework SourceType** convention (dotnet/sdk#53135) to
let the SDK handle materialization, rather than manual Copy tasks:

1. `DefineStaticWebAssets` registers pass-through files with
`SourceType="Framework"` (using their NuGet cache paths)
2. `UpdatePackageStaticWebAssets` materializes them to per-project
`obj/fx/{SourceId}/` directories, transforming metadata:
   - `SourceType` → `Discovered`
   - `SourceId` → project `PackageId`
   - `BasePath` → project `StaticWebAssetBasePath`
   - `AssetMode` → `CurrentProject`
   - `Identity` → materialized file path (unique per project)

**Asset classification:**
- **Pass-through files** (JS, maps, ICU, native wasm, DLLs when WebCil
disabled): Framework → materialized by SDK task
- **WebCil-converted files**: `SourceType="Computed"` (already
per-project in `obj/webcil/`)

This eliminates manual Copy/culture-handling logic from Browser.targets
and properly models the relationship: framework assets adopted by each
consuming project.

## Dependencies

Requires SDK support for `SourceType="Framework"` from dotnet/sdk#53135.

## Testing

Validated with patched SDK (Framework SourceType +
MaterializeFrameworkAsset) + Browser.targets:
- aspnetcore Components.TestServer (3 WASM clients): ✅ Build succeeded
  - WasmMinimal: 22 framework + 205 webcil files
  - WasmRemoteAuthentication: 20 framework + 204 webcil files
  - BasicTestApp: 29 framework + 218 webcil files
- Multi-client build (2 WASM clients): ✅ Framework assets materialized
per project
- Multi-client publish: ✅ Both clients produce fingerprinted files
- Incremental rebuild: ✅ Timestamp-based skip
- WebCil disabled (`WasmEnableWebcil=false`): ✅

## Related

- Fixes crash introduced by #124125
- Depends on dotnet/sdk#53135 (Framework SourceType support)
- Supersedes #125309 (copy-based approach)

Co-authored-by: Copilot <[email protected]>
lewing added a commit to dotnet/runtime that referenced this pull request Apr 2, 2026
…add test (#126211)

## Summary

Fixes the publish-time static web assets pipeline for multi-client
hosted Blazor WASM scenarios and adds a test.

## Root Cause

`ComputeWasmPublishAssets.GroupResolvedFilesToPublish` processes all
`ResolvedFileToPublish` items — including those marked
`CopyToPublishDirectory=Never` (like the HotReload dll from
`_WasmImplicitlyReferenceHotReload`). When it finds a matching filename,
it treats the `ResolvedFileToPublish` item as a "linked version" and
replaces the build-time Framework-materialized asset (which has a unique
per-project path from `UpdatePackageStaticWebAssets`) with the raw SDK
path.

In multi-client hosted scenarios, both Client1 and Client2 get their
materialized copies replaced by the same raw SDK path →
`ApplyCompressionNegotiation` crashes on a duplicate key in
`ToAssetDictionary`.

## Fix

Skip `CopyToPublishDirectory=Never` items in
`GroupResolvedFilesToPublish`. These items were explicitly marked as not
for publish — they should not be used to replace build-time assets.
Build-time Framework-materialized assets (#125329) keep their unique
per-project Identity through publish.

## Test

Adds `MultiClientHostedBuildAndPublish` test with a dedicated
`BlazorMultiClientHosted` test asset (Server + Client1 + Client2).
Parametrized over Debug/Release × build/publish. Verifies:
- Both clients' framework directories exist
- Both contain `dotnet.*.js` and `dotnet.native*.wasm` files (publish
only)
- No duplicate Identity crash in `ApplyCompressionNegotiation`

## Related

- Completes work from #125329 (build-time Framework materialization)
- Depends on dotnet/sdk#53135 (Framework SourceType infrastructure)
- Traced via binlog: `ComputeWasmPublishAssets` replaces materialized
per-client asset with raw SDK-path `CopyToPublishDirectory=Never` item →
duplicate key in server's `ApplyCompressionNegotiation`

---------

Co-authored-by: Copilot <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-AspNetCore RazorSDK, BlazorWebAssemblySDK, StaticWebAssetsSDK

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants