PAYATHON 2026

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 应用,导致资源无法按普通库的方式传递给宿主。

需要修改两处配置:

  1. 将实际启动进程的项目改为使用 Microsoft.NET.Sdk.Web。
  2. 将 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}/... 访问,除非另行定义了资源路径。

备注:内容仅供参考。