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:
- References a Razor class library that contributes JS library modules (
*.lib.module.js) and/or scoped CSS.
- 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:
- 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.
- 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
Summary
Microsoft.AspNetCore.Components.WebViewships a fallbackblazor.modules.jsonthat is selected via a static web asset group (BlazorWebViewModules=fallback) plus a target that promotes the SDK-generated JS modules manifest toAssetKind=All. This authoring causes a publish-time crash in MAUI Blazor Hybrid apps that reference a Razor class library: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.jsonas a Framework static web asset, the same wayMicrosoft.AspNetCore.Components.WebAssemblyships its framework assets. No SDK change is required.Affected scenario
A MAUI Blazor Hybrid app (
Microsoft.NET.Sdk.Razor) that:*.lib.module.js) and/or scoped CSS.Microsoft.AspNetCore.Components.WebView(.Maui), which contributes the fallback_framework/blazor.modules.json.dotnet buildsucceeds, butdotnet publishthrows.Root cause
During build, the WebView package's
StaticWebAssets.Groups.targets:_TagSdkModulesManifestWithGrouppromotes the SDK-generated JS modules manifest fromAssetKind=Build/AssetMode=CurrentProjecttoAssetKind=All/AssetMode=All, tagging itBlazorWebViewModules=default._ResolveBlazorWebViewModulesGroupresolves the deferredBlazorWebViewModulesgroup 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 deferredBlazorWebViewModulesgroup is not resolved and the fallback is not excluded. The publish asset set for_framework/blazor.modules.jsonthen contains:blazor.modules.json(package fallback)BlazorWebViewModules=fallbackjsmodules.build.manifest.json(promoted)BlazorWebViewModules=defaultjsmodules.publish.manifest.jsonGenerateStaticWebAssetEndpointsManifest.ComputeManifestAssetsgroups by target path and callsChooseNearestAssetKind(group, "Publish").SingleOrDefault(). With twoAllassets,ChooseNearestAssetKindyields both andSingleOrDefault()throws.Proposed fix (idiomatic, in this repo)
Model
blazor.modules.jsonas a Framework asset instead of using theBlazorWebViewModulesgroup + manifest-promotion machinery — exactly howMicrosoft.AspNetCore.Components.WebAssemblyships 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:and delete
StaticWebAssets.Groups.targets(theBlazorWebViewModulesgroup definitions,_TagSdkModulesManifestWithGroup, and_ResolveBlazorWebViewModulesGroup). Keep theJSModuleManifestRelativePath=_framework/blazor.modules.jsonandCompressionEnabled=falseproperties 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 underobj/.../fx/). From then on it is an ordinary project asset that flows through publish without re-introducing a cross-projectPackageasset.All; the app's generated manifest isBuild/Publish. At publish there is a singleAllplus thePublishasset on the route, soChooseNearestAssetKindpicks thePublishasset cleanly — no two-Allcollision, no crash. The app's manifest wins._framework/blazor.modules.jsonis the one that is served — the fallback still works.Two important details:
StaticWebAssetBasePath=/with_framework/in the relative path (i.e. ship the file atwwwroot/_framework/blazor.modules.json). WithBasePath=/_frameworkand a root-level relative path, materialization rebases to the consumer'sBasePath(/) and the asset lands atblazor.modules.jsoninstead of_framework/blazor.modules.json.Components.WebAssemblyalready follows theBasePath=/+_framework/-in-relative-path convention.**/blazor.modules.jsonframework 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:
dotnet publishfails withSequence contains more than one element.blazor.modules.jsonmodeled as a Framework asset →dotnet publishsucceeds 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/MauiBlazorHybridWebViewin dotnet/sdk and can serve as a reference for the expected behavior.)Related
GenerateStaticWebAssetEndpointsManifestthrowsSequence contains more than one elementwhen a MAUI Blazor Hybrid project references a Razor class library and the BlazorWebView package sdk#54779 — theSequence contains more than one elementcrash (root cause analysis above).JSModulesEnabled=false.