
Aspire 集成独立 Blazor WebAssembly基于 Aspire.Hosting.Blazor 网关实现服务发现与全链路可观测【免费下载链接】aspireAspire is the tool for code-first, extensible, observable dev and deploy.项目地址: https://gitcode.com/GitHub_Trending/as/aspire本文以 Aspire 仓库中的playground/BlazorStandalone示例README为主线系统讲解如何在不依赖 Blazor Server 宿主的情况下将独立standaloneBlazor WebAssembly应用接入 Aspire通过Aspire.Hosting.Blazor包自动生成 YARP 反向代理网关Gateway统一承载 WASM 静态资源、API 代理与浏览器遥测转发实现服务发现、分布式追踪与结构化日志三大能力。读完本文你将掌握AddBlazorWasmProject/AddBlazorGateway的完整用法、网关自动化的底层原理以及 WASM 客户端接入配置下发、服务发现桥接和 OpenTelemetry 遥测上报的完整实战链路。为什么独立 Blazor WebAssembly 需要网关Aspire 的服务发现Service Discovery、遥测上报等能力天然面向运行在服务器端的进程。对于Hosted Blazor应用Blazor Server 既是宿主也是转发层一切水到渠成但独立 Blazor WebAssemblystandalone WASM运行在浏览器沙箱中没有服务端宿主进程会直接面临三个问题服务地址如何下发WASM 应用无法读取宿主机环境变量后端 API 的地址含端口无法通过常规配置通道获得跨域CORS如何规避浏览器直连后端 API 与 OTLP 端点会触发跨域限制而浏览器无法使用 gRPC 传输 OTLP遥测如何上报WASM 没有IHostedService自动启动机制OpenTelemetry 提供者不会自动运行需要手动初始化。Aspire.Hosting.Blazor集成给出的答案是自动生成一个网关——一个由 Aspire 编排层动态装配的 ASP.NET Core YARP 反向代理进程它同时充当静态文件服务器、API 代理和 OTLP 遥测代理让浏览器端的所有请求都保持同源。整体架构网关为中心的拓扑示例的架构可以用下面的 mermaid 图概括来自 playground/BlazorStandalone/README.md整个拓扑由三类资源构成WASM 客户端资源名app独立 Blazor WebAssembly 应用从/_blazor/_configuration拉取配置通过服务发现解析weatherapi遥测经网关代理上报网关资源名gatewayAspire.Hosting.Blazor自动生成的 ASP.NET Core YARP 代理完成四件事——按路径前缀如/app/托管 WASM 静态文件、暴露/_blazor/_configuration配置端点、将/weatherapi/*反代到后端服务、将/_otlp/*遥测转发到 Aspire Dashboard后端服务weatherapi、timeapi普通 Web API 项目被网关通过服务发现解析并代理。这种浏览器 → 网关 → 后端/Dashboard的单跳拓扑使得 CORS 完全不再需要——浏览器看到的只有网关这一个源same origin。核心 API注册 WASM 应用与网关集成由Aspire.Hosting.Blazor包提供。在 AppHost 目录中使用 Aspire CLI 添加aspire add Aspire.Hosting.Blazor注意Blazor 网关 API 目前标记为实验性ASPIREBLAZOR001C# 调用方需按 src/Aspire.Hosting.Blazor/README.md 的要求用#pragma warning disable ASPIREBLAZOR001显式启用。C# 用法示例 AppHost 的完整注册代码位于 BlazorStandalone.AppHost/AppHost.cs比 README 中的最小示例更进一步同时演示了命名端点引用与scheme 解析两种服务引用方式var builder DistributedApplication.CreateBuilder(args); // 方式一普通引用scheme-based resolution网关按 https/http 解析全部端点 var weatherApi builder.AddProjectProjects.BlazorStandalone_WeatherApi(weatherapi); // 方式二指定命名端点named endpoint——仅该端点会被转发到网关 var timeApi builder.AddProjectProjects.BlazorStandalone_TimeApi(timeapi) .WithHttpsEndpoint(name: api); // 注册独立 WASM 应用资源名成为 URL 路径前缀app → 托管在 /app/ // WithReference 声明服务依赖网关会据此生成 YARP 路由 var blazorApp builder.AddBlazorWasmProjectProjects.BlazorStandalone(app) .WithReference(weatherApi) .WithReference(timeApi.GetEndpoint(api)); // 网关托管 WASM 静态文件代理 API 与 OTLP 流量 var gateway builder.AddBlazorGateway(gateway) .WithExternalHttpEndpoints() .WithOtlpExporter(OtlpProtocol.HttpProtobuf) .WithBlazorClientApp(blazorApp); builder.Build().Run();对应的最小版本来自 src/Aspire.Hosting.Blazor/README.md#pragma warning disable ASPIREBLAZOR001 var builder DistributedApplication.CreateBuilder(args); var api builder.AddProjectProjects.ApiService(api); var blazor builder.AddBlazorWasmProjectProjects.BlazorApp(web) .WithReference(api); builder.AddBlazorGateway(gateway) .WithBlazorClientApp(blazor) .WithExternalHttpEndpoints(); builder.Build().Run(); #pragma warning restore ASPIREBLAZOR001TypeScriptpolyglot AppHost用法同样的能力也暴露给了 TypeScript AppHost见 src/Aspire.Hosting.Blazor/README.mdimport { createBuilder } from ./.aspire/modules/aspire.mjs; const builder await createBuilder(); const api await builder.addProject(api, ../ApiService/ApiService.csproj); const blazor await builder.addBlazorWasmProject(web, ../BlazorApp/BlazorApp.csproj) .withReference(await api.getEndpoint(http)); await builder.addBlazorGateway(gateway) .withBlazorClientApp(blazor) .withExternalHttpEndpoints(); await builder.build().run();API 语义要点从 BlazorGatewayExtensions.cs 的源码签名可以提炼出几个关键语义API语义AddBlazorWasmProjectTProject(name)通过IProjectMetadata发现项目路径并注册为BlazorWasmAppResource资源名即 URL 路径前缀该资源不直接启动进程ExcludeFromManifest初始状态为WaitingAddBlazorWasmApp(name, projectPath)等价 API但显式传入项目路径ATS 导出名addBlazorWasmProjectAddBlazorGateway(name)将随包发布的Scripts/Gateway.cs注册为 C# 应用资源AddCSharpApp自带 HTTP/HTTPS 端点发布模式下自动生成 Dockerfile基于mcr.microsoft.com/dotnet/sdk/aspnetWithBlazorClientApp(wasmApp, apiPrefix _api, otlpPrefix _otlp, proxyTelemetry true)把 WASM 客户端挂到网关自动转发服务引用、生成 YARP 路由与客户端配置、把 WASM 资源设为网关子资源以镜像生命周期状态并注册浏览器调试支持其中两个默认路径前缀定义在 GatewayConfigurationBuilder.csDefaultApiPrefix _apiAPI 代理路径段路由形如/app/_api/weatherapi/{**catch-all}DefaultOtlpPrefix _otlpOTLP 遥测代理路径段路由形如/app/_otlp/{**catch-all}。WithBlazorClientApp内部还会做两件重要的事把 WASM 应用置为网关资源的子资源Parent gateway.Resource并由MirrorGatewayStateToClients通过ResourceNotificationService.WatchAsync监听网关状态将 Running/Stopped 等状态与客户端 URL{gatewayUrl}/{pathPrefix}镜像到 WASM 资源上这就是 Dashboard 资源页里能直接看到app访问地址的原因。网关自动生成编排期的四步装配网关不是手写的项目而是托管层在启动时动态装配的。按 BlazorGatewayExtensions.cs 中的WithBlazorApp流程编排期会执行以下四步构建并读取 WASM 项目清单调用BlazorWasmAppBuilder.BuildAsync构建 WASM 项目随后通过 MSBuild 属性读取staticwebassets.build.json/*.staticwebassets.endpoints.json等清单定位静态文件生成网关脚本与路由把Scripts/Gateway.cs模板见 Scripts/Gateway.cs.in注册为 C# 应用同时由GatewayConfigurationBuilder.EmitProxyConfiguration把 YARP 的 Route/Cluster 配置写成环境变量ReverseProxy__Routes__*、ReverseProxy__Clusters__*构建客户端配置 JSON生成webAssembly.environment格式的配置响应ClientApps__{app}__ConfigResponse其中服务地址以__ORIGIN__占位符 相对路径的形式拼接运行时解析为网关真实来源启动网关以项目资源方式启动该 C# 应用并通过环境变量注入上述全部配置。网关运行时行为由 Gateway.cs.in 决定核心逻辑如下读取ClientApps配置段为每个注册的 WASM 客户端MapGet其配置端点/{prefix}/_blazor/_configuration返回application/json读取ReverseProxy配置段AddReverseProxy().LoadFromConfig(...)加载 YARP 路由并注册AddServiceDiscoveryDestinationResolver()让 YARP 的 Destination 通过 Aspire 服务发现services__{name}__{scheme}__{index}环境变量解析通过MapGroup(appConfig.PathPrefix).MapStaticAssets(endpointsManifest)按路径前缀托管静态资源UseStaticWebAssets()加载合并后的运行时清单仅对Sec-Fetch-Dest: document的顶级导航做 HTTP→HTTPS 重定向避免重定向程序化 API/遥测请求从而保证 OTLP 与服务请求同源。一个值得注意的细节运行时清单合并由EndpointsManifestTransformer.MergeRuntimeManifestsAsync完成输出到.aspire/blazor/gateways/{name}/output/merged.staticwebassets.runtime.json并通过staticWebAssets环境变量传给网关——多个 WASM 客户端的静态资产清单会在网关侧被合并成一份。配置下发/_blazor/_configuration 端点网关为每个客户端暴露一个/{prefix}/_blazor/_configuration端点返回 WASM 客户端所需的全部运行时配置。README 中给出的响应示例为{ webAssembly: { environment: { services__weatherapi__https__0: https://localhost:7101, services__weatherapi__http__0: http://localhost:5101, OTEL_EXPORTER_OTLP_ENDPOINT: https://localhost:65269/_otlp/, OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf, OTEL_SERVICE_NAME: app, OTEL_EXPORTER_OTLP_HEADERS: x-otlp-api-key... } } }JSON 结构在 ClientConfiguration.cs 中定义根对象为webAssembly.environment即一个Dictionarystring, string其键名刻意使用双下划线分隔符services__weatherapi__https__0这样在 WASM 端可以直接与 .NET 配置系统的冒号层级services:weatherapi:https:0对应。结合 GatewayConfigurationBuilder.cs 的BuildConfigExpression实现可以澄清几个关键点服务地址是相对路径而非绝对 URL源码生成的是__ORIGIN__/app/_api/weatherapi形式的表达式__ORIGIN__在运行时由网关的 HTTPS优先或 HTTP 端点替换。因此客户端拿到的地址解析后指向网关自身这正是规避 CORS 的核心手段OTLP 端点只下发路径段OTEL_EXPORTER_OTLP_ENDPOINT /app/_otlp客户端用页面来源HostEnvironment.BaseAddress解析为完整地址即POST {origin}/app/_otlp/v1/traces同样落在网关上协议固定为http/protobufWASM 环境无法使用 gRPC浏览器遥测统一走 OTLP/HTTPAPI Key 不下发到浏览器需要指出的是README 示例 JSON 中出现的OTEL_EXPORTER_OTLP_HEADERS字段在当前仓库源码实现中有意不下发——它包含 Dashboard 的 OTLP API Key而该配置是浏览器可见的 JSON。取而代之的是由 YARP 代理在服务端根据网关自身的OTEL_EXPORTER_OTLP_HEADERS环境变量生成RequestHeader/Settransform在转发遥测时注入认证头避免密钥泄露到浏览器端见EmitYarpRoutes中对应的注释与实现。客户端接入JavaScript Initializer 注入环境变量独立 WASM 应用拿不到服务器环境变量因此需要一个桥ClientServiceDefaults库自带一个 JavaScript initializeronRuntimeConfigLoaded在 .NET 运行时配置MonoConfig加载完成后执行。完整实现见 BlazorStandalone.ClientServiceDefaults.lib.module.jslet aspireConfig null; export async function onRuntimeConfigLoaded(config) { try { // Resolve relative to base href so it works under path prefixes (e.g., /app/_blazor/_configuration) const configUrl new URL(_blazor/_configuration, document.baseURI).href; const response await fetch(configUrl); if (response.ok) { const serverConfig await response.json(); aspireConfig serverConfig; const envVars serverConfig?.webAssembly?.environment; if (envVars Object.keys(envVars).length 0) { config.environmentVariables ?? {}; for (const [key, value] of Object.entries(envVars)) { config.environmentVariables[key] value; } } } } catch (error) { console.warn(Failed to load Aspire client configuration:, error); } }要点相对路径解析new URL(_blazor/_configuration, document.baseURI)基于base href解析因此无论客户端被托管在/app/还是其他前缀下都能正确命中网关端点注入目标把服务端配置写入config.environmentVariables即 MonoConfig 的环境变量从而 WASM 端可通过Environment.GetEnvironmentVariable()读取生命周期WASM 场景使用onRuntimeConfigLoaded而非 Hosted Blazor 场景的beforeWebAssemblyStart这是两者接入方式的核心差异之一。服务发现桥接环境变量 → IConfiguration环境变量虽然可用Environment.GetEnvironmentVariable()读取但不会自动进入IConfiguration。而 Aspire 的 Service Discovery 恰恰是从IConfiguration读取services__{name}__{scheme}__{index}配置的因此需要显式桥接。示例 WASM 客户端 Program.cs 的做法var builder WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.AddApp(#app); builder.RootComponents.AddHeadOutlet(head::after); // 桥接环境变量到 IConfiguration // services__weatherapi__https__0 → services:weatherapi:https:0 builder.Configuration.AddEnvironmentVariables(); // 添加 Aspire 客户端服务默认值OpenTelemetry、服务发现、韧性 builder.AddBlazorClientServiceDefaults(); // 默认 HttpClient指向页面来源 builder.Services.AddScoped(sp new HttpClient { BaseAddress new Uri(builder.HostEnvironment.BaseAddress) }); // 命名 HttpClient基于服务发现解析 weatherapi builder.Services.AddHttpClient(weatherapi, client { client.BaseAddress new Uri(httpshttp://weatherapi); }); // 命名 HttpClient基于服务发现解析 timeapi builder.Services.AddHttpClient(timeapi, client { client.BaseAddress new Uri(httpshttp://timeapi); }); var host builder.Build();其中httpshttp://weatherapi是 Aspire 服务发现的标准 scheme 语法优先解析 HTTPS 端点回退到 HTTP。命名端点场景则使用httpshttp://_endpointName.serviceName形式YARP 网关侧也使用该格式解析 Destination。AddBlazorClientServiceDefaults()的实现位于 BlazorStandalone.ClientServiceDefaults/Extensions.cs它做四件事注册 Blazor 组件指标与追踪源AddComponentsMetrics/AddComponentsTracing对应Microsoft.AspNetCore.Components与.Lifecycle两个 meter/source配置客户端 OpenTelemetryConfigureBlazorClientOpenTelemetry详见下一节AddServiceDiscovery()注册服务发现核心ConfigureHttpClientDefaults为所有 HttpClient 统一挂载服务发现。页面组件侧的使用见 Pages/Weather.razor通过IHttpClientFactory.CreateClient(weatherapi)获取命名客户端再直接GetFromJsonAsyncWeatherForecast[](weatherforecast)调用 API——地址已在注册时由服务发现解析为网关的/app/_api/weatherapi相对路径。遥测链路浏览器 → 网关 → DashboardClientServiceDefaults把 OpenTelemetry 配置为向 OTLP 端点即网关/_otlp/代理发送日志、指标与追踪网关再转发给 Aspire Dashboard。数据流如下来自 README 的 sequence diagram强制初始化 Telemetry Provider关键陷阱WebAssembly 不会自动启动IHostedService因此 OpenTelemetry 的TelemetryHostedService永远不会执行MeterProvider与TracerProvider必须手动强制实例化仓库中注释引用了 dotnet/aspire issue #2816var host builder.Build(); // WebAssembly 不支持 IHostedServiceTelemetryHostedService 不会启动 // 必须手动强制初始化 MeterProvider 与 TracerProvider _ host.Services.GetServiceMeterProvider(); _ host.Services.GetServiceTracerProvider(); await host.RunAsync();在 Program.cs 中该初始化发生在builder.Build()之后、RunAsync()之前与 README 中的说明完全一致。客户端 OTLP 导出器的实现细节ConfigureBlazorClientOpenTelemetry见 Extensions.cs中的几个工程化细节值得展开无 OTLP 端点则静默跳过OTEL_EXPORTER_OTLP_ENDPOINT为空时直接返回避免在无网关直连的场景下报错OTLP 重试韧性管道构建一个匹配 OTLP retry spec 的 Polly 管道——初始退避 1s、最大 5s、最多重试 3 次、指数退避 抖动jitter、遵循 429/503 的Retry-After头可重试状态码为 408/429/5xx是 OTLP 规范 429/502/503/504 的超集经IPostConfigureOptionsOtlpExporterOptions注入 HttpClientFactory让 traces、metrics、logs 三个信号的所有 OTLP exporter 实例共享同一个带后台导出处理器BackgroundExportHandler的 HttpClient固定 serviceInstanceId资源构建时使用AddService(serviceName, serviceInstanceId: serviceName)使所有浏览器标签页在 Dashboard 中聚合为同一个服务实例而不是每个标签页产生一条独立记录端点拼接baseAddress otlpPathBase /v1/logs等把相对路径解析到页面来源下。客户端项目依赖见 BlazorStandalone.ClientServiceDefaults.csprojMicrosoft.AspNetCore.Components.WebAssembly、Microsoft.Extensions.ServiceDiscovery、Microsoft.Extensions.Http.Resilience、OpenTelemetry.Exporter.OpenTelemetryProtocol、OpenTelemetry.Extensions.Hosting、OpenTelemetry.Instrumentation.Http并声明SupportedPlatform Includebrowser /。项目结构总览示例解决方案BlazorStandalone.slnx的组织方式见 playground/BlazorStandaloneBlazorStandalone/ ├── BlazorStandalone.AppHost/ # Aspire 编排器 │ └── Program.cs # AddBlazorWasmProject AddBlazorGateway │ ├── BlazorStandalone/ # 独立 Blazor WASM 客户端 │ ├── Program.cs # AddEnvironmentVariables() 服务发现 │ └── Pages/Weather.razor # 通过 HttpClientFactory 调用 WeatherAPI │ ├── BlazorStandalone.ClientServiceDefaults/ # WASM 端遥测 配置 │ ├── Extensions.cs # AddBlazorClientServiceDefaults() │ ├── BackgroundExportHandler.cs # WASM 自定义 OTLP 后台导出处理器 │ └── wwwroot/*.lib.module.js # JS initializer拉取 /_blazor/_configuration │ ├── BlazorStandalone.ServiceDefaults/ # 服务端 Aspire 默认值 │ └── Extensions.cs # 标准 AddServiceDefaults() │ ├── BlazorStandalone.WeatherApi/ # 示例 API/weatherforecast ├── BlazorStandalone.TimeApi/ # 示例 API演示命名端点引用 └── Directory.Packages.props # 集中包版本管理与 README 结构对照可见仓库实际还额外包含TimeApi用于验证WithReference(timeApi.GetEndpoint(api))命名端点转发与BlazorStandalone.ServiceDefaults服务端标准默认值。AppHost 中还有一段#if !SKIP_DASHBOARD_REFERENCE包裹的代码仅用于 playground 调试 Dashboard 时把Aspire.Dashboard作为项目资源加入终端用户代码不需要可用/p:SkipDashboardReferencetrue关闭以体验真实发布行为。运行与验证按 README 的步骤运行cd BlazorStandalone.AppHost dotnet run随后使用控制台输出中的登录 URL 打开Aspire Dashboard在 Resources 页面点击gateway的 URL并追加路径/app/进入 WASM 应用点击Weather页面触发一次经 YARP 代理的 API 调用回到 Dashboard 查看遥测结构化日志Structured Logs来自gateway服务端与appWASM 客户端的日志追踪Traces完整的分布式链路app→gateway→weatherapi浏览器端的组件渲染 span、HTTP 调用 span 与网关代理 span、API span 串成一条完整 trace。验证时值得留意的行为Dashboard 中浏览器端app的所有标签页会聚合为同一服务实例serviceInstanceId固定gateway与weatherapi的日志来自服务端app的日志来自浏览器端经 OTLP 代理上报二者会同时出现在日志视图中。与 Hosted Blazor 的关键差异README 末尾用一张对照表总结了两种形态的差异这是选择架构时的重要参考AspectHosted BlazorStandalone with GatewayServerBlazor Server hosts WASMAuto-generated Gateway hosts WASMConfig deliveryDOM comment in rendered HTML/_blazor/_configurationendpointJS initializerbeforeWebAssemblyStartonRuntimeConfigLoadedTelemetry proxyThrough servers/_otlp/*routeThrough gateways/_otlp/*routeService discoveryWorks out of the boxRequiresAddEnvironmentVariables()Client discriminator(client)suffix on service nameSeparate resource name (e.g.,app)CORSNot needed (same origin)Not needed (gateway is same origin)核心结论独立 WASM 的接入成本集中在三处——环境变量桥接、JS initializer、强制初始化 Telemetry Provider其余配置端点、YARP 路由、静态托管、OTLP 转发全部由Aspire.Hosting.Blazor自动完成。适用场景与限制从源码结构可以推断该集成尤其适合以下场景前后端完全分离的 Blazor WebAssembly 项目希望零 CORS 配置接入 Aspire 服务发现与可观测性一个网关托管多个WASM 客户端应用GatewayAppsAnnotation支持在同一个网关上注册多个客户端各自以资源名为路径前缀需要Debug in Browser调试体验的场景WithBlazorClientApp会自动注册浏览器调试资源可用WithBlazorDebuggerBrowser(msedge | chrome)指定调试浏览器。同时需注意当前限制Blazor 网关 API 处于实验期ASPIREBLAZOR001若使用AddDotnetProjectBlazorGatewayrun/watch 能力的 .NET 资源形态发布publish暂不支持发布场景应使用AddBlazorGateway详见 BlazorGatewayExtensions.cs 中AddDotnetProjectBlazorGateway的说明。发布模式下编排器会为每个 WASM 客户端生成 publish 伴生资源BlazorWasmPublishResource通过 Dockerfile 在镜像内完成dotnet publish、静态资产加前缀拷贝与端点清单改写PrefixEndpoints.cs最终把产物合入网关镜像。【免费下载链接】aspireAspire is the tool for code-first, extensible, observable dev and deploy.项目地址: https://gitcode.com/GitHub_Trending/as/aspire创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考