Skip to content

BlazorWebView: model blazor.modules.json as a Framework asset (fixes publish-time Sequence contains more than one element in MAUI Blazor Hybrid + RCL) #67374

Description

@javiercn

Summary

Microsoft.AspNetCore.Components.WebView ships a fallback blazor.modules.json that is selected via a static web asset group (BlazorWebViewModules=fallback) plus a target that promotes the SDK-generated JS modules manifest to AssetKind=All. This authoring causes a publish-time crash in MAUI Blazor Hybrid apps that reference a Razor class library:

error : InvalidOperationException: Sequence contains more than one element
   at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source)
   at Microsoft.AspNetCore.StaticWebAssets.Tasks.GenerateStaticWebAssetEndpointsManifest.ComputeManifestAssets(...)
   at Microsoft.AspNetCore.StaticWebAssets.Tasks.GenerateStaticWebAssetEndpointsManifest.Execute()

This is the root cause of dotnet/sdk#54779 (and the .NET MAUI gallery regression dotnet/maui#35953). The crash is in the SDK task, but the fix belongs here: the package should model blazor.modules.json as a Framework static web asset, the same way Microsoft.AspNetCore.Components.WebAssembly ships its framework assets. No SDK change is required.

Affected scenario

A MAUI Blazor Hybrid app (Microsoft.NET.Sdk.Razor) that:

  1. References a Razor class library that contributes JS library modules (*.lib.module.js) and/or scoped CSS.
  2. References Microsoft.AspNetCore.Components.WebView(.Maui), which contributes the fallback _framework/blazor.modules.json.

dotnet build succeeds, but dotnet publish throws.

Root cause

During build, the WebView package's StaticWebAssets.Groups.targets:

  • _TagSdkModulesManifestWithGroup promotes the SDK-generated JS modules manifest from AssetKind=Build/AssetMode=CurrentProject to AssetKind=All/AssetMode=All, tagging it BlazorWebViewModules=default.
  • _ResolveBlazorWebViewModulesGroup resolves the deferred BlazorWebViewModules group based on _ExistingBuildJSModules, so the package fallback (BlazorWebViewModules=fallback) is excluded when the app generates its own manifest. Build works.

During publish, group filtering runs with SkipDeferred="true" (Microsoft.NET.Sdk.StaticWebAssets.Publish.targets), so the deferred BlazorWebViewModules group is not resolved and the fallback is not excluded. The publish asset set for _framework/blazor.modules.json then contains:

Asset SourceType AssetKind AssetGroups
blazor.modules.json (package fallback) Package All BlazorWebViewModules=fallback
jsmodules.build.manifest.json (promoted) Computed All BlazorWebViewModules=default
jsmodules.publish.manifest.json Computed Publish (empty)

GenerateStaticWebAssetEndpointsManifest.ComputeManifestAssets groups by target path and calls ChooseNearestAssetKind(group, "Publish").SingleOrDefault(). With two All assets, ChooseNearestAssetKind yields both and SingleOrDefault() throws.

Proposed fix (idiomatic, in this repo)

Model blazor.modules.json as a Framework asset instead of using the BlazorWebViewModules group + manifest-promotion machinery — exactly how Microsoft.AspNetCore.Components.WebAssembly ships its framework assets (StaticWebAssetBasePath=/, StaticWebAssetFrameworkPattern=**/*.js, no groups, no fallback/default variants — materialization alone handles it).

Concretely, in src/Components/WebView/WebView/src/Microsoft.AspNetCore.Components.WebView.csproj:

<PropertyGroup>
  <!-- Use BasePath '/' with the '_framework/' prefix in the asset's relative path so the path
       survives materialization into the consuming project (consumer BasePath is '/'). -->
  <StaticWebAssetBasePath>/</StaticWebAssetBasePath>
  <!-- Include the modules manifest in the framework pattern alongside the JS files. -->
  <StaticWebAssetFrameworkPattern>**/*.js;**/blazor.modules.json</StaticWebAssetFrameworkPattern>
</PropertyGroup>

<ItemGroup>
  <!-- ship under wwwroot/_framework/blazor.modules.json -->
  <Content Update="wwwroot\_framework\blazor.modules.json"
    AssetTraitName="JSModule" AssetTraitValue="JSModuleManifest" />
</ItemGroup>

and delete StaticWebAssets.Groups.targets (the BlazorWebViewModules group definitions, _TagSdkModulesManifestWithGroup, and _ResolveBlazorWebViewModulesGroup). Keep the JSModuleManifestRelativePath=_framework/blazor.modules.json and CompressionEnabled=false properties in the package's build props.

Why this works

The framework asset is materialized once into the consuming project at build (SourceType → Discovered, AssetMode=CurrentProject, materialized under obj/.../fx/). From then on it is an ordinary project asset that flows through publish without re-introducing a cross-project Package asset.

  • App has its own JS modules: the materialized fallback is All; the app's generated manifest is Build/Publish. At publish there is a single All plus the Publish asset on the route, so ChooseNearestAssetKind picks the Publish asset cleanly — no two-All collision, no crash. The app's manifest wins.
  • App has no JS modules: no manifest is generated, so the materialized fallback at _framework/blazor.modules.json is the one that is served — the fallback still works.

Two important details:

  1. Use StaticWebAssetBasePath=/ with _framework/ in the relative path (i.e. ship the file at wwwroot/_framework/blazor.modules.json). With BasePath=/_framework and a root-level relative path, materialization rebases to the consumer's BasePath (/) and the asset lands at blazor.modules.json instead of _framework/blazor.modules.json. Components.WebAssembly already follows the BasePath=/ + _framework/-in-relative-path convention.
  2. The **/blazor.modules.json framework pattern is matched against the relative path without fingerprint tokens; this works regardless of fingerprinting.

Repro / validation

I reproduced the crash and validated the fix with a self-contained integration test in dotnet/sdk (no SDK code change), using a packed package that simulates the WebView package and a Razor class library:

  • Group authoring (current) → dotnet publish fails with Sequence contains more than one element.
  • blazor.modules.json modeled as a Framework asset → dotnet publish succeeds with a single _framework/blazor.modules.json (the app's generated manifest), and the fallback-only case (no app modules) is served by the materialized package manifest.

(The test lives in test/Microsoft.NET.Sdk.StaticWebAssets.Tests/MauiBlazorHybridWebViewIntegrationTest.cs + test/TestAssets/TestProjects/MauiBlazorHybridWebView in dotnet/sdk and can serve as a reference for the expected behavior.)

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-blazorIncludes: Blazor, Razor Components

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions