Blazor web app 作为 Library 被其他 Assembly 加载后返回 404
结论
问题在于 static web assets 的构建清单,与 wwwroot 目录本身无关。
在 .NET 10 中,MapStaticAssets() 依赖宿主项目构建时生成的 static web assets endpoint manifest。当前启动进程的是 console app,但该项目使用 Microsoft.NET.Sdk,因此不会以 Web 宿主的方式处理引用项目中的静态资源。Blazor 项目虽然已改为 Library,其 Web SDK 项目模式仍可能将它视为根 Web 应用,导致资源无法按普通库的方式传递给宿主。
需要修改两处配置:
- 将实际启动进程的项目改为使用
Microsoft.NET.Sdk.Web。 - 将 Blazor Library 的
StaticWebAssetProjectMode设置为Default。
_framework/blazor.web.js 不属于 Blazor 项目 wwwroot 中的普通文件,应由 ASP.NET Core 的 static web assets 管道提供,无需手工复制。
修改宿主项目
console app 是实际入口项目,应改用 Web SDK:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BlazorApp\BlazorApp.csproj" />
</ItemGroup>
</Project>
使用 Microsoft.NET.Sdk.Web 不会影响程序以 console app 或 Windows service 方式运行。它会引入 ASP.NET Core 的构建目标,并在构建和发布时生成完整的 static web assets 清单。
入口代码可以保持不变:
Console.WriteLine("Start running the web-app");
Console.WriteLine("--------------------------------------------");
await new WebAppRunner().Run();
修改 Blazor Library
Blazor 项目可以继续使用 Web SDK,但需要将它明确配置为供其他项目引用的库,而非最终宿主:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
<OutputType>Library</OutputType>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
</PropertyGroup>
</Project>
原先为 wwwroot 文件添加的以下设置可以删除:
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
static web assets 有独立的资源发现、指纹、压缩和发布流程。将 CSS、JavaScript 等文件作为普通内容文件复制到输出目录,无法生成 MapStaticAssets() 所需的 endpoint manifest,也不会生成 _framework/blazor.web.js。
调整 WebApplication 的创建方式
ApplicationName 应使用入口程序集,因此不要将它改为 Blazor Library 的程序集名称。static web assets 清单属于实际宿主项目,如果强制指定库程序集名称,运行时可能会查找错误的清单。
应用可以这样创建:
using BlazorApp.Components;
public sealed class WebAppRunner
{
public async Task Run()
{
var builder = WebApplication.CreateBuilder(
new WebApplicationOptions
{
ContentRootPath = AppContext.BaseDirectory
});
builder.Services
.AddRazorComponents()
.AddInteractiveServerComponents();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
await app.RunAsync();
}
}
需要注意以下配置:
- 不设置
ApplicationName,由框架使用宿主项目的入口程序集。 - 将
ContentRootPath设为AppContext.BaseDirectory。Windows service 使用该路径比依赖当前工作目录更可靠。 - 无需手工设置
WebRootPath。 UseStaticFiles()无法解决_framework/blazor.web.js的问题,因此不必为此调用它。- 不要调用
UsePathBase("/wwwroot")。wwwroot是物理资源目录,不是应用的 URL 前缀。 - 保留
MapStaticAssets()。
如果要直接运行 dotnet run 的构建输出,而非发布目录中的文件,可以根据运行环境保留:
builder.WebHost.UseStaticWebAssets();
该调用只能加载构建过程已经生成的 static web assets 信息。它不能代替 Web SDK,也无法解决宿主项目没有生成清单的问题。
为什么原来的检查可能产生误导
以下代码不能准确判断 MapStaticAssets() 是否已经映射资源:
var fileInfo = app.Environment.WebRootFileProvider
.GetFileInfo("_framework/blazor.web.js");
MapStaticAssets() 根据构建生成的 endpoint manifest 映射资源,而这些资源不一定会表现为 WebRootFileProvider 下的普通物理文件。因此,即使 Exists 为 false,也不能据此认定对应的 HTTP endpoint 不存在。
更直接的验证方法,是构建宿主项目后请求实际地址:
https://localhost:<port>/_framework/blazor.web.js
还可以检查宿主输出目录中是否生成了与宿主程序集对应的 static web assets 文件,例如:
ConsoleApp.staticwebassets.endpoints.json
ConsoleApp.staticwebassets.runtime.json
实际生成的文件取决于构建方式和 SDK 版本,不宜依赖某个固定的文件组合。清单应以宿主项目为中心生成,并包含引用项目中的资源。
构建和发布方式
构建和发布都应从宿主项目执行。
构建命令:
dotnet clean
dotnet build ConsoleApp/ConsoleApp.csproj
发布命令:
dotnet publish ConsoleApp/ConsoleApp.csproj -c Release
只发布 Blazor Library 后再手工复制 DLL 无法完成资源合并,因为 static web assets 会在宿主项目的构建或发布阶段合并。
修改 SDK 和项目模式后,建议删除两个项目的 bin、obj 目录,再重新构建,以免旧版 static web assets 清单残留。
动态插件需要额外注意
上述方案适用于宿主项目在构建阶段通过 ProjectReference 或 NuGet 包引用 Blazor Library 的情况。MSBuild 可以在此过程中发现库中的静态资源,并将它们合并到宿主清单。
如果 Windows service 只在运行时通过 Assembly.Load* 加载一个构建时未知的插件,宿主构建阶段无法发现该插件,static web assets 管道也无法自动将插件资源写入宿主清单。这类运行时插件需要选用明确的资源交付方式:
- 将插件发布为 Razor Class Library,并由宿主在构建阶段引用。
- 由插件注册自己的 endpoint 或
IFileProvider,明确提供物理资源或嵌入资源。 - 在安装插件时部署资源,再通过
UseStaticFiles(new StaticFileOptions { ... })映射指定目录。
采用动态插件方案时,/_framework/blazor.web.js 仍由 Web 宿主提供,不应从插件的 wwwroot 手工复制。插件自身的静态文件通常按照 Razor Class Library 规则,通过 /_content/{PackageId}/... 访问,除非另行定义了资源路径。
备注:内容仅供参考。
