Skip to content

Commit d23e53a

Browse files
github-actions[bot]lewingCopilot
authored
[release/11.0-preview3] Fix publish-time Framework materialization for multi-client WASM and add test (#126378)
Backport of #126211 to release/11.0-preview3 /cc @lewing ## Customer Impact - [ ] Customer reported - [ ] Found internally [Select one or both of the boxes. Describe how this issue impacts customers, citing the expected and actual behaviors and scope of the issue. If customer-reported, provide the issue number.] ## Regression - [ ] Yes - [ ] No [If yes, specify when the regression was introduced. Provide the PR or commit if known.] ## Testing [How was the fix verified? How was the issue missed previously? What tests were added?] ## Risk [High/Medium/Low. Justify the indication by mentioning how risks were measured and addressed.] **IMPORTANT**: If this backport is for a servicing release, please verify that: - For .NET 8 and .NET 9: The PR target branch is `release/X.0-staging`, not `release/X.0`. - For .NET 10+: The PR target branch is `release/X.0` (no `-staging` suffix). ## Package authoring no longer needed in .NET 9 **IMPORTANT**: Starting with .NET 9, you no longer need to edit a NuGet package's csproj to enable building and bump the version. Keep in mind that we still need package authoring in .NET 8 and older versions. --------- Co-authored-by: Larry Ewing <lewing@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e7c4426 commit d23e53a

11 files changed

Lines changed: 103 additions & 69 deletions

File tree

src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,13 @@ Copyright (c) .NET Foundation. All rights reserved.
753753

754754
<!-- remove the original assemblies -->
755755
<ResolvedFileToPublish Remove="@(WasmAssembliesToBundle)" />
756-
<_WasmResolvedFilesToPublish Include="@(ResolvedFileToPublish)" />
756+
<!-- Exclude items marked CopyToPublishDirectory=Never. These are build-only assets
757+
(e.g., HotReload dll) that should not participate in publish. Without this filter,
758+
ComputeWasmPublishAssets matches them by filename and replaces Framework-materialized
759+
per-project assets with the raw SDK-path original, causing duplicate Identity crashes
760+
in multi-client hosted publish scenarios. -->
761+
<_WasmResolvedFilesToPublish Include="@(ResolvedFileToPublish)"
762+
Condition="'%(ResolvedFileToPublish.CopyToPublishDirectory)' != 'Never'" />
757763
</ItemGroup>
758764

759765
<ComputeWasmPublishAssets

src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs

Lines changed: 50 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -103,79 +103,61 @@ public void BugRegression_60479_WithRazorClassLib()
103103
}
104104

105105
[Theory]
106-
[InlineData(Configuration.Debug)]
107-
[InlineData(Configuration.Release)]
108-
public void MultiClientHostedBuild(Configuration config)
106+
[InlineData(Configuration.Debug, false)]
107+
[InlineData(Configuration.Release, false)]
108+
[InlineData(Configuration.Debug, true)]
109+
[InlineData(Configuration.Release, true)]
110+
public void MultiClientHostedBuildAndPublish(Configuration config, bool publish)
109111
{
110-
// Test that two Blazor WASM client projects can be hosted by a single server project
111-
// without duplicate static web asset Identity collisions. This validates the Framework
112-
// SourceType materialization path that gives each client unique per-project Identity
113-
// for shared runtime pack files.
114-
ProjectInfo info = CopyTestAsset(config, aot: false, TestAsset.BlazorBasicTestApp, "multi_hosted");
115-
116-
// _projectDir is now .../App. Go up to the root and create a second client + server.
117-
string rootDir = Path.GetDirectoryName(_projectDir)!;
118-
string client1Dir = _projectDir;
119-
string client2Dir = Path.Combine(rootDir, "App2");
120-
string serverDir = Path.Combine(rootDir, "Server");
121-
122-
// Duplicate App as App2 with a different StaticWebAssetBasePath
123-
Utils.DirectoryCopy(client1Dir, client2Dir);
124-
string client2Csproj = Path.Combine(client2Dir, "BlazorBasicTestApp.csproj");
125-
File.Move(client2Csproj, Path.Combine(client2Dir, "BlazorBasicTestApp2.csproj"));
126-
client2Csproj = Path.Combine(client2Dir, "BlazorBasicTestApp2.csproj");
127-
128-
// Set different base paths so the two clients don't collide on routes
129-
AddItemsPropertiesToProject(Path.Combine(client1Dir, "BlazorBasicTestApp.csproj"),
130-
extraProperties: "<StaticWebAssetBasePath>client1</StaticWebAssetBasePath>");
131-
AddItemsPropertiesToProject(client2Csproj,
132-
extraProperties: "<StaticWebAssetBasePath>client2</StaticWebAssetBasePath><RootNamespace>BlazorBasicTestApp</RootNamespace>");
133-
134-
// Create a minimal server project that references both clients
135-
Directory.CreateDirectory(serverDir);
136-
string serverCsproj = Path.Combine(serverDir, "Server.csproj");
137-
File.WriteAllText(serverCsproj, $"""
138-
<Project Sdk="Microsoft.NET.Sdk.Web">
139-
<PropertyGroup>
140-
<TargetFramework>{DefaultTargetFrameworkForBlazor}</TargetFramework>
141-
<Nullable>enable</Nullable>
142-
<ImplicitUsings>enable</ImplicitUsings>
143-
</PropertyGroup>
144-
<ItemGroup>
145-
<ProjectReference Include="..\App\BlazorBasicTestApp.csproj" />
146-
<ProjectReference Include="..\App2\BlazorBasicTestApp2.csproj" />
147-
</ItemGroup>
148-
</Project>
149-
""");
150-
File.WriteAllText(Path.Combine(serverDir, "Program.cs"), """
151-
var builder = WebApplication.CreateBuilder(args);
152-
var app = builder.Build();
153-
app.UseStaticFiles();
154-
app.Run();
155-
""");
156-
157-
// Build the server project — this will transitively build both clients.
158-
// Without Framework materialization, this would fail with duplicate Identity
159-
// for shared runtime pack files (dotnet.native.js, ICU data, etc.)
160-
string logPath = Path.Combine(s_buildEnv.LogRootPath, info.ProjectName, $"{info.ProjectName}-multi-hosted.binlog");
112+
// Test that two Blazor WASM client projects can be built/published by a single server
113+
// project without duplicate static web asset Identity collisions. This validates the
114+
// Framework SourceType materialization path that gives each client unique per-project
115+
// Identity for shared runtime pack files.
116+
string id = publish ? "multi_pub" : "multi_hosted";
117+
CopyTestAsset(config, aot: false, TestAsset.BlazorMultiClientHosted, id);
118+
119+
string serverDir = _projectDir;
120+
string rootDir = Path.GetDirectoryName(serverDir)!;
121+
string client1Dir = Path.Combine(rootDir, "Client1");
122+
string client2Dir = Path.Combine(rootDir, "Client2");
123+
124+
string command = publish ? "publish" : "build";
125+
string logPath = Path.Combine(_logPath, $"{id}-{config}-{command}.binlog");
161126
using ToolCommand cmd = new DotNetCommand(s_buildEnv, _testOutput)
162127
.WithWorkingDirectory(serverDir);
163-
CommandResult result = cmd
128+
_ = cmd
164129
.WithEnvironmentVariable("NUGET_PACKAGES", _nugetPackagesDir)
165-
.ExecuteWithCapturedOutput("build", $"-p:Configuration={config}", $"-bl:{logPath}")
130+
.ExecuteWithCapturedOutput(command, $"-p:Configuration={config}", $"-bl:{logPath}")
166131
.EnsureSuccessful();
167132

168-
// Verify both clients produced framework files in their own bin directories
169-
string client1Framework = Path.Combine(client1Dir, "bin", config.ToString(), DefaultTargetFrameworkForBlazor, "wwwroot", "_framework");
170-
string client2Framework = Path.Combine(client2Dir, "bin", config.ToString(), DefaultTargetFrameworkForBlazor, "wwwroot", "_framework");
171-
172-
Assert.True(Directory.Exists(client1Framework), $"Client1 framework dir missing: {client1Framework}");
173-
Assert.True(Directory.Exists(client2Framework), $"Client2 framework dir missing: {client2Framework}");
174-
175-
// Both should have dotnet.js (verifies framework files were materialized per-client)
176-
var client1Files = Directory.GetFiles(client1Framework);
177-
var client2Files = Directory.GetFiles(client2Framework);
178-
Assert.Contains(client1Files, f => Path.GetFileName(f).StartsWith("dotnet.") && f.EndsWith(".js"));
179-
Assert.Contains(client2Files, f => Path.GetFileName(f).StartsWith("dotnet.") && f.EndsWith(".js"));
133+
if (publish)
134+
{
135+
string publishDir = Path.Combine(serverDir, "bin", config.ToString(), DefaultTargetFrameworkForBlazor, "publish");
136+
string client1Framework = Path.Combine(publishDir, "wwwroot", "client1", "_framework");
137+
string client2Framework = Path.Combine(publishDir, "wwwroot", "client2", "_framework");
138+
139+
Assert.True(Directory.Exists(client1Framework), $"Client1 publish framework dir missing: {client1Framework}");
140+
Assert.True(Directory.Exists(client2Framework), $"Client2 publish framework dir missing: {client2Framework}");
141+
142+
var client1Files = Directory.GetFiles(client1Framework);
143+
var client2Files = Directory.GetFiles(client2Framework);
144+
Assert.Contains(client1Files, f => Path.GetFileName(f).StartsWith("dotnet.") && f.EndsWith(".js"));
145+
Assert.Contains(client2Files, f => Path.GetFileName(f).StartsWith("dotnet.") && f.EndsWith(".js"));
146+
Assert.Contains(client1Files, f => Path.GetFileName(f).Contains("dotnet.native") && f.EndsWith(".wasm"));
147+
Assert.Contains(client2Files, f => Path.GetFileName(f).Contains("dotnet.native") && f.EndsWith(".wasm"));
148+
}
149+
else
150+
{
151+
string client1Framework = Path.Combine(client1Dir, "bin", config.ToString(), DefaultTargetFrameworkForBlazor, "wwwroot", "_framework");
152+
string client2Framework = Path.Combine(client2Dir, "bin", config.ToString(), DefaultTargetFrameworkForBlazor, "wwwroot", "_framework");
153+
154+
Assert.True(Directory.Exists(client1Framework), $"Client1 framework dir missing: {client1Framework}");
155+
Assert.True(Directory.Exists(client2Framework), $"Client2 framework dir missing: {client2Framework}");
156+
157+
var client1Files = Directory.GetFiles(client1Framework);
158+
var client2Files = Directory.GetFiles(client2Framework);
159+
Assert.Contains(client1Files, f => Path.GetFileName(f).StartsWith("dotnet.") && f.EndsWith(".js"));
160+
Assert.Contains(client2Files, f => Path.GetFileName(f).StartsWith("dotnet.") && f.EndsWith(".js"));
161+
}
180162
}
181163
}

src/mono/wasm/Wasm.Build.Tests/BrowserStructures/TestAsset.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ public class TestAsset
66
public static readonly TestAsset BlazorBasicTestApp = new() { Name = "BlazorBasicTestApp", RunnableProjectSubPath = "App" };
77
public static readonly TestAsset LibraryModeTestApp = new() { Name = "LibraryMode" };
88
public static readonly TestAsset BlazorWebWasm = new() { Name = "BlazorWebWasm", RunnableProjectSubPath = "BlazorWebWasm" };
9+
public static readonly TestAsset BlazorMultiClientHosted = new() { Name = "BlazorMultiClientHosted", RunnableProjectSubPath = "Server" };
910
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<h1>Client 1</h1>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
2+
<PropertyGroup>
3+
<TargetFramework>net11.0</TargetFramework>
4+
<StaticWebAssetBasePath>client1</StaticWebAssetBasePath>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0-alpha.2.25073.4" />
10+
</ItemGroup>
11+
</Project>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
2+
var builder = WebAssemblyHostBuilder.CreateDefault(args);
3+
await builder.Build().RunAsync();
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<h1>Client 2</h1>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
2+
<PropertyGroup>
3+
<TargetFramework>net11.0</TargetFramework>
4+
<StaticWebAssetBasePath>client2</StaticWebAssetBasePath>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0-alpha.2.25073.4" />
10+
</ItemGroup>
11+
</Project>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
2+
var builder = WebAssemblyHostBuilder.CreateDefault(args);
3+
await builder.Build().RunAsync();
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
<PropertyGroup>
3+
<TargetFramework>net11.0</TargetFramework>
4+
<Nullable>enable</Nullable>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
</PropertyGroup>
7+
<ItemGroup>
8+
<ProjectReference Include="..\Client1\Client1.csproj" />
9+
<ProjectReference Include="..\Client2\Client2.csproj" />
10+
</ItemGroup>
11+
</Project>

0 commit comments

Comments
 (0)