Add Aspire.Hosting.Blazor integration for Blazor WebAssembly apps#15691
Open
javiercn wants to merge 25 commits intomicrosoft:mainfrom
Open
Add Aspire.Hosting.Blazor integration for Blazor WebAssembly apps#15691javiercn wants to merge 25 commits intomicrosoft:mainfrom
javiercn wants to merge 25 commits intomicrosoft:mainfrom
Conversation
Port the Blazor WASM hosting integration from MvpSummitDemo.Hosting into the Aspire repo as Aspire.Hosting.Blazor. New APIs: - AddBlazorGateway: Creates a C# script-based YARP gateway that serves WASM static files and proxies API/OTLP traffic - AddBlazorWasmApp/AddBlazorWasmProject<T>: Registers Blazor WASM apps as Aspire resources with subpath-prefixed URLs - WithClient: Connects a WASM app to the gateway, auto-forwarding service references and configuring YARP routes - WithBlazorApp: Registers apps with the gateway and configures environment Gateway features: - Serves WASM static files under /<resource-name>/ prefix - Proxies service discovery references via YARP - Forwards OTLP telemetry from browser to Aspire dashboard - Emits client configuration at /_blazor/_configuration Telemetry noise fixes: - Filter static asset requests (_framework/, _content/) from gateway traces - Filter OTLP export requests (_otlp/) from gateway traces - Filter OTLP export POSTs (v1/traces, v1/metrics, v1/logs) from WASM client HTTP instrumentation to prevent feedback loop - Pass OTEL_SERVICE_NAME through client config so WASM apps show with their resource name in the dashboard Playground updates (AspireWithBlazorStandalone): - Updated AppHost to use new Aspire.Hosting.Blazor APIs - Fixed base href to match resource path prefix (/app/) - Fixed JS initializer to resolve _blazor/_configuration relative to base href for correct subpath serving Tests: 29 unit tests covering resource creation, annotation wiring, environment variable emission, YARP config generation, and manifest transformation.
…nore attributes
- GatewayConfigurationBuilder now emits services__name__https__0 format
directly instead of services:name:https:0, eliminating the need for
client-side key transformation in the JS initializer
- JS initializer passes env var keys through without replaceAll(':', '__')
- Add [AspireExportIgnore] to all public extension methods (ASPIREEXPORT008)
- Suppress IDE0031 false positive in DevelopmentManifest.cs
…layground sample - BlazorHostedExtensions.cs: ProxyService() and ProxyTelemetry() extension methods for hosted Blazor Web App projects that need to proxy service calls and OTLP telemetry from the WebAssembly client via YARP - BlazorHostedExtensionsTests.cs: 8 tests covering YARP routes, client config, OTLP routes, combined scenarios, multiple services, path prefix absence - playground/AspireWithBlazorHosted/: complete hosted Blazor sample with AppHost, Server (YARP + config endpoint), Client, WeatherApi, ServiceDefaults
…ce fixes, client discriminator, DPP cleanup
…ion component
- Add DefaultApiPrefix ('_api') and DefaultOtlpPrefix ('_otlp') constants
- Thread apiPrefix/otlpPrefix params through all hosting APIs:
WithClient(), WithBlazorApp(), ProxyService(), ProxyTelemetry()
- Add ApiPrefix/OtlpPrefix to GatewayAppRegistration and HostedClientAnnotation
- Extract DOM comment config into BlazorClientConfiguration.razor component
- Add test for custom prefix routing (38 tests passing)
Config is now delivered via DOM comments in BlazorClientConfiguration.razor, making the HTTP endpoint unnecessary for the hosted model.
Contributor
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 15691Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 15691" |
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new Aspire.Hosting.Blazor hosting integration that brings Blazor WebAssembly apps into Aspire orchestration via a same-origin YARP proxy (standalone gateway or hosted-server injection), with service discovery and OTLP forwarding, plus unit tests and playgrounds demonstrating both hosting models.
Changes:
- Introduces
src/Aspire.Hosting.Blazorwith resources/annotations, gateway + hosted extension APIs, manifest transformation, and supporting scripts. - Adds
tests/Aspire.Hosting.Blazor.Testsvalidating app registration, gateway configuration emission, and hosted proxy behavior. - Adds standalone + hosted playground solutions illustrating API proxying and browser OTLP export flow.
Reviewed changes
Copilot reviewed 122 out of 127 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| Aspire.slnx | Adds the new hosting project, tests project, and Blazor playground projects to the repo solution. |
| src/Aspire.Hosting.Blazor/Aspire.Hosting.Blazor.csproj | New packable hosting integration project; includes scripts as content. |
| src/Aspire.Hosting.Blazor/BlazorGatewayExtensions.cs | Adds gateway + WASM project registration APIs and wiring for env/config emission. |
| src/Aspire.Hosting.Blazor/BlazorHostedExtensions.cs | Adds hosted-model APIs (ProxyService, ProxyTelemetry) that emit env/YARP config. |
| src/Aspire.Hosting.Blazor/GatewayConfigurationBuilder.cs | Builds client config JSON and YARP route/cluster env vars for gateway/hosted models. |
| src/Aspire.Hosting.Blazor/BlazorWasmAppBuilder.cs | Builds WASM projects and queries MSBuild for static web asset manifest paths. |
| src/Aspire.Hosting.Blazor/BlazorGatewayLog.cs | Centralized logger messages for build/manifest/transform operations. |
| src/Aspire.Hosting.Blazor/ClientConfiguration.cs | Typed JSON model for the Blazor WASM runtime configuration response. |
| src/Aspire.Hosting.Blazor/Manifests/AppManifestPaths.cs | Holds per-app manifest path info for gateway processing. |
| src/Aspire.Hosting.Blazor/Manifests/DevelopmentManifest.cs | Typed model for static web assets runtime manifest merging. |
| src/Aspire.Hosting.Blazor/Manifests/EndpointsManifest.cs | Typed model for static web assets endpoints manifest transformation. |
| src/Aspire.Hosting.Blazor/Manifests/EndpointsManifestTransformer.cs | Prefixes asset paths and merges runtime manifests for multi-app gateway hosting. |
| src/Aspire.Hosting.Blazor/Manifests/ManifestJsonContext.cs | Source-gen JSON context for manifest/client config serialization. |
| src/Aspire.Hosting.Blazor/Manifests/MSBuildPropertiesOutput.cs | Typed model for dotnet msbuild -getProperty JSON output. |
| src/Aspire.Hosting.Blazor/Resources/BlazorWasmAppResource.cs | Public resource representing a WASM project (metadata-only). |
| src/Aspire.Hosting.Blazor/Resources/BlazorWasmPublishResource.cs | Publish-time companion resource for producing WASM outputs into the gateway container. |
| src/Aspire.Hosting.Blazor/Resources/GatewayAppsAnnotation.cs | Gateway annotation tracking attached WASM apps and their proxy configuration. |
| src/Aspire.Hosting.Blazor/Scripts/Gateway.cs | File-based ASP.NET Core gateway app (YARP + static assets + config endpoint). |
| src/Aspire.Hosting.Blazor/Scripts/PrefixEndpoints.cs | Script to prefix endpoints and add SPA fallback in publish scenarios. |
| tests/Aspire.Hosting.Blazor.Tests/Aspire.Hosting.Blazor.Tests.csproj | New test project for the Blazor hosting integration. |
| tests/Aspire.Hosting.Blazor.Tests/AddBlazorWasmAppTests.cs | Unit tests for WASM resource creation and annotations. |
| tests/Aspire.Hosting.Blazor.Tests/WithBlazorAppTests.cs | Unit tests for gateway annotation/registration and reference forwarding. |
| playground/AspireWithBlazorStandalone/Directory.Packages.props | Pins Blazor WASM package versions for the standalone playground. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.slnx | Standalone playground solution definition. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.AppHost/AspireWithBlazorStandalone.AppHost.csproj | Standalone playground AppHost wiring in the new integration and sample resources. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.AppHost/Program.cs | Uses AddBlazorWasmProject + AddBlazorGateway + WithClient to run the standalone model. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.AppHost/appsettings.json | AppHost logging configuration for the standalone playground. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.AppHost/appsettings.Development.json | Dev logging configuration for the standalone playground. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.AppHost/Properties/launchSettings.json | Launch settings for standalone playground AppHost. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone/AspireWithBlazorStandalone.csproj | Standalone WASM client project definition. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone/Program.cs | Standalone WASM client bootstrapping + env var bridging + telemetry initialization. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone/App.razor | Standalone client routing layout. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone/_Imports.razor | Imports for standalone client. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone/Pages/Home.razor | Standalone client home page. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone/Pages/Counter.razor | Standalone client counter page. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone/Pages/Weather.razor | Standalone client weather page calling backend via service discovery. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone/Layout/MainLayout.razor | Standalone client main layout. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone/Layout/MainLayout.razor.css | Standalone client main layout styles. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone/Layout/NavMenu.razor | Standalone client nav menu. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone/Layout/NavMenu.razor.css | Standalone client nav menu styles. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone/Properties/launchSettings.json | Standalone client launch settings. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone/wwwroot/index.html | Standalone client HTML shell (with <base href="/app/">). |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone/wwwroot/css/app.css | Standalone client CSS. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone/wwwroot/favicon.png | Standalone client favicon asset. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone/wwwroot/icon-192.png | Standalone client icon asset. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.ClientServiceDefaults/AspireWithBlazorStandalone.ClientServiceDefaults.csproj | Standalone client “service defaults” library for WASM telemetry + service discovery. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.ClientServiceDefaults/wwwroot/AspireWithBlazorStandalone.ClientServiceDefaults.lib.module.js | Standalone JS initializer to fetch config and inject env vars into runtime. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.ClientServiceDefaults/Telemetry/CircularBuffer.cs | Utility buffer for WASM batch export processing. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.ClientServiceDefaults/Telemetry/TaskBasedBatchExportProcessor.cs | WASM-friendly batch export processor implementation. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.ClientServiceDefaults/Telemetry/WebAssemblyOtlpTraceExporter.cs | OTLP/HTTP trace exporter for WASM. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.ClientServiceDefaults/Telemetry/WebAssemblyOtlpLogExporter.cs | OTLP/HTTP log exporter for WASM. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.ClientServiceDefaults/Telemetry/WebAssemblyOtlpMetricExporter.cs | OTLP/HTTP metric exporter for WASM. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.ClientServiceDefaults/Telemetry/Serializer/ProtobufWireType.cs | Protobuf wire constants used by custom OTLP serializers. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.ClientServiceDefaults/Telemetry/Serializer/OtlpTraceFieldNumbers.cs | OTLP trace protobuf field numbers. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.ClientServiceDefaults/Telemetry/Serializer/OtlpLogFieldNumbers.cs | OTLP log protobuf field numbers. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.ClientServiceDefaults/Telemetry/Serializer/OtlpMetricFieldNumbers.cs | OTLP metric protobuf field numbers. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.ServiceDefaults/AspireWithBlazorStandalone.ServiceDefaults.csproj | Standalone server-side “service defaults” library for sample services. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.ServiceDefaults/Extensions.cs | Adds health endpoints, OTLP, service discovery, and a configuration endpoint for WASM clients. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.WeatherApi/AspireWithBlazorStandalone.WeatherApi.csproj | Standalone weather API sample project. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.WeatherApi/Program.cs | Standalone weather API sample endpoint setup. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.WeatherApi/appsettings.json | Standalone weather API logging config. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.WeatherApi/appsettings.Development.json | Standalone weather API dev logging config. |
| playground/AspireWithBlazorStandalone/AspireWithBlazorStandalone.WeatherApi/Properties/launchSettings.json | Standalone weather API launch settings. |
| playground/AspireWithBlazorHosted/Directory.Packages.props | Pins packages to net8-compatible versions for the hosted playground model. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.slnx | Hosted playground solution definition. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.AppHost/AspireWithBlazorHosted.AppHost.csproj | Hosted playground AppHost project. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.AppHost/Program.cs | Uses ProxyService + ProxyTelemetry on the hosted server project. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.AppHost/Properties/launchSettings.json | Hosted playground AppHost launch settings. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Server/AspireWithBlazorHosted.csproj | Hosted server project (Blazor Web App + YARP). |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Server/Program.cs | Hosted server wiring for Razor components + YARP + service discovery. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Server/appsettings.json | Hosted server app settings. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Server/appsettings.Development.json | Hosted server dev settings. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Server/Properties/launchSettings.json | Hosted server launch settings. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Server/wwwroot/app.css | Hosted server static CSS. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Server/wwwroot/favicon.png | Hosted server favicon. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Server/Components/App.razor | Hosted server HTML shell and boot script. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Server/Components/Routes.razor | Hosted routing setup. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Server/Components/_Imports.razor | Hosted component imports. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Server/Components/BlazorClientConfiguration.razor | Embeds base64 client config as a DOM comment during prerender. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Server/Components/Pages/Home.razor | Hosted home page. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Server/Components/Pages/Error.razor | Hosted error page. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Server/Components/Layout/MainLayout.razor | Hosted main layout. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Server/Components/Layout/MainLayout.razor.css | Hosted main layout styles. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Server/Components/Layout/NavMenu.razor | Hosted nav menu. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Server/Components/Layout/NavMenu.razor.css | Hosted nav menu styles. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Client/AspireWithBlazorHosted.Client.csproj | Hosted-model WASM client project. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Client/Program.cs | Hosted-model WASM client bootstrapping + env var bridging + telemetry initialization. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Client/_Imports.razor | Hosted-model client imports. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Client/Pages/Counter.razor | Hosted-model client counter page (interactive WASM). |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Client/Pages/Weather.razor | Hosted-model client weather page (interactive WASM). |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Client/wwwroot/appsettings.json | Hosted-model client config. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.Client/wwwroot/appsettings.Development.json | Hosted-model client dev config. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.ClientServiceDefaults/AspireWithBlazorHosted.ClientServiceDefaults.csproj | Hosted-model client “service defaults” library for WASM telemetry + service discovery. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.ClientServiceDefaults/wwwroot/AspireWithBlazorHosted.ClientServiceDefaults.lib.module.js | JS initializer supporting both hosted DOM-comment config and standalone endpoint fetch. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.ClientServiceDefaults/Telemetry/CircularBuffer.cs | Utility buffer for hosted-model WASM batch export processing. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.ClientServiceDefaults/Telemetry/TaskBasedBatchExportProcessor.cs | WASM-friendly batch export processor for hosted model. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.ClientServiceDefaults/Telemetry/WebAssemblyOtlpTraceExporter.cs | OTLP/HTTP trace exporter for hosted-model WASM. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.ClientServiceDefaults/Telemetry/WebAssemblyOtlpLogExporter.cs | OTLP/HTTP log exporter for hosted-model WASM. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.ClientServiceDefaults/Telemetry/WebAssemblyOtlpMetricExporter.cs | OTLP/HTTP metric exporter for hosted-model WASM. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.ClientServiceDefaults/Telemetry/Serializer/ProtobufWireType.cs | Protobuf wire constants used by hosted-model custom OTLP serializers. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.ClientServiceDefaults/Telemetry/Serializer/OtlpTraceFieldNumbers.cs | OTLP trace protobuf field numbers for hosted model. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.ClientServiceDefaults/Telemetry/Serializer/OtlpLogFieldNumbers.cs | OTLP log protobuf field numbers for hosted model. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.ClientServiceDefaults/Telemetry/Serializer/OtlpMetricFieldNumbers.cs | OTLP metric protobuf field numbers for hosted model. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.ServiceDefaults/AspireWithBlazorHosted.ServiceDefaults.csproj | Hosted playground server-side “service defaults” library. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.ServiceDefaults/Extensions.cs | Hosted playground service defaults helpers (health/OTLP/service discovery). |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.WeatherApi/AspireWithBlazorHosted.WeatherApi.csproj | Hosted playground weather API sample project. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.WeatherApi/Program.cs | Hosted playground weather API endpoint setup. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.WeatherApi/appsettings.json | Hosted playground weather API config. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.WeatherApi/appsettings.Development.json | Hosted playground weather API dev config. |
| playground/AspireWithBlazorHosted/AspireWithBlazorHosted.WeatherApi/Properties/launchSettings.json | Hosted playground weather API launch settings. |
davidfowl
reviewed
Mar 29, 2026
| @@ -0,0 +1,29 @@ | |||
| var builder = DistributedApplication.CreateBuilder(args); | |||
Contributor
There was a problem hiding this comment.
This should be called AppHost.cs
Contributor
|
Did you look at the existing yarp resource to see if we should layer on top for those scenarios? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Aspire.Hosting.Blazorextends Aspire's orchestration to Blazor WebAssembly applications, giving browser-based .NET apps the same service discovery, distributed tracing, and structured logging that server-side Aspire resources enjoy.With a few lines in the AppHost, a Blazor WASM client can:
https+http://weatherapi)Approach: same-origin reverse proxy
The integration routes all browser traffic through a YARP reverse proxy that runs on the same origin as the WASM app. This is a deliberate architectural choice:
Access-Control-Allow-Originheaders are needed on backend services or the OTLP collectorgraph LR subgraph Browser["Browser (same origin)"] WASM["Blazor WASM"] end subgraph Proxy["Proxy (same origin as WASM)"] YARP["YARP Reverse Proxy"] end subgraph Backend["Internal Network"] API["Backend APIs"] OTLP["Aspire Dashboard<br/>(OTLP Collector)"] end WASM -- "/_api/weatherapi/*" --> YARP WASM -- "/_otlp/v1/traces" --> YARP YARP -- "Service Discovery" --> API YARP -- "OTLP Forward" --> OTLP style Browser fill:#e8f4fd style Proxy fill:#fff3cd style Backend fill:#f0f0f0The proxy is either:
Two hosting models
Aspire.Hosting.Blazorsupports both models that Blazor WebAssembly ships with, each with a tailored API.Standalone WebAssembly
The WASM app has no ASP.NET Core server. Aspire generates a gateway process that serves static files and proxies traffic.
Hosted WebAssembly (Blazor Web App)
The WASM client is hosted inside an ASP.NET Core server. The server already serves static files — it just needs YARP routes injected.
Architecture
Standalone model
graph TB subgraph AppHost["Aspire AppHost"] Dashboard["Aspire Dashboard<br/>(OTLP + UI)"] Gateway["gateway<br/>(auto-generated YARP proxy)"] WeatherAPI["weatherapi<br/>(Web API)"] end subgraph Browser WASM["Blazor WASM Client<br/>served under /app/"] end WASM -- "GET /app/_blazor/_configuration<br/>(service URLs + OTLP config)" --> Gateway WASM -- "GET /app/_api/weatherapi/*<br/>(API proxy)" --> Gateway WASM -- "POST /app/_otlp/v1/traces<br/>(telemetry proxy)" --> Gateway Gateway -- "Static files<br/>/app/**" --> Browser Gateway -- "HTTP reverse proxy" --> WeatherAPI Gateway -- "OTLP forward" --> DashboardThe gateway is a real ASP.NET Core process launched by the AppHost. Its
Program.csis auto-generated as adotnet run --filescript that configures YARP from environment variables.Multi-app support. Multiple WASM apps can share a single gateway. Each app is served under its own path prefix (the resource name), so
/store/and/admin/coexist without conflict. The<base href>in each app'sindex.htmlmust match its prefix.Hosted model
graph TB subgraph AppHost["Aspire AppHost"] Dashboard["Aspire Dashboard<br/>(OTLP + UI)"] BlazorApp["blazorapp<br/>(Blazor Web App)"] WeatherAPI["weatherapi<br/>(Web API)"] end subgraph Browser WASM["WebAssembly Client Components"] end BlazorApp -- "Prerender + serve WASM" --> Browser WASM -- "/_api/weatherapi/*<br/>(YARP proxy)" --> BlazorApp WASM -- "/_otlp/v1/traces<br/>(OTLP proxy)" --> BlazorApp BlazorApp -- "HTTP reverse proxy" --> WeatherAPI BlazorApp -- "OTLP forward" --> DashboardNo gateway process is created — the existing Blazor Server acts as the proxy. The AppHost injects YARP route configuration via environment variables, and the server's
Program.csloads them withLoadFromConfig.Design details
Same-origin proxy vs. CORS
The proxy approach was chosen over CORS for several reasons:
AllowOriginheadersEnvironment variables as the configuration transport
The .NET WebAssembly runtime supports setting environment variables at boot time. This lets us reuse the standard Aspire patterns:
services__weatherapi__https__0→ service discovery viaIConfigurationOTEL_EXPORTER_OTLP_ENDPOINT→ OpenTelemetry SDK auto-configurationOTEL_SERVICE_NAME→ resource identification in the dashboardBy mapping Aspire's env-var conventions into the WASM runtime, client-side code can use the same service discovery and telemetry APIs as server-side code.
AddEnvironmentVariables()bridges them intoIConfiguration, and service discovery resolveshttps+http://weatherapias usual.Configuration delivery: DOM comments vs. HTTP endpoint
The two models have different constraints for delivering configuration to the WASM client:
Standalone: The WASM app loads from static files. The gateway serves a
/_blazor/_configurationJSON endpoint. A JavaScript initializer (onRuntimeConfigLoaded) fetches this endpoint and injects env vars into the MonoConfig before the runtime starts.Hosted: The server pre-renders the page. The WASM runtime needs configuration before it boots, but
beforeWebAssemblyStartfires before any fetch could complete. Instead, the server embeds the config as a base64-encoded HTML comment during prerendering:<!--Blazor-Client-Config:eyJ3ZWJBc3NlbWJseSI6ey4uLn19-->The JS initializer reads this synchronously from the DOM — no network round-trip, no race condition with the WASM boot sequence.
sequenceDiagram participant Server participant Browser participant WASM as WASM Runtime Server->>Browser: Prerendered HTML with <!--Blazor-Client-Config:BASE64--> Browser->>Browser: beforeWebAssemblyStart() reads DOM comment Browser->>WASM: configureRuntime() injects env vars WASM->>WASM: Boot with service URLs + OTLP configExplicit opt-in with
ProxyService()In the hosted model,
WithReference(weatherApi)makes a service available to the server.ProxyService(weatherApi)additionally makes it available to the WASM client by creating a YARP route.These are intentionally separate — not every service should be reachable from the browser. A database connection string referenced by the server should never be proxied to the client. The developer explicitly chooses which services the browser can reach.
Configurable route prefixes
The default prefixes
/_apiand/_otlpare chosen to minimize collision with application routes. However, if an app has routes starting with/_api/, they can be customized:The proxy then routes
/{prefix}/api-proxy/weatherapi/*(standalone) or/api-proxy/weatherapi/*(hosted) instead of the defaults.Client identity in the dashboard
In the hosted model, the server and WASM client share the same resource name (
blazorapp). To distinguish their telemetry in the dashboard, the hosting layer appends(client)to theOTEL_SERVICE_NAMEfor the WASM client:blazorapp→ server-side traces and logsblazorapp (client)→ browser-side traces and logsThis lets you filter and correlate telemetry by origin while seeing the full distributed trace across both.
Data flow
Configuration delivery
flowchart LR subgraph Orchestration["AppHost (orchestration time)"] Hosting["Aspire.Hosting.Blazor"] end subgraph Runtime["Runtime"] Proxy["Gateway / Blazor Server"] JS["JS Initializer"] Mono["WASM Runtime"] App["Blazor App Code"] end Hosting -- "env vars:<br/>ReverseProxy__*, Client__*" --> Proxy Proxy -- "JSON endpoint or<br/>DOM comment" --> JS JS -- "Environment variables" --> Mono Mono -- "IConfiguration<br/>(AddEnvironmentVariables)" --> App App -- "Service Discovery<br/>(https+http://weatherapi)" --> ProxyAPI request flow (standalone)
sequenceDiagram participant WASM as Blazor WASM (/app/) participant GW as Gateway (YARP) participant API as weatherapi WASM->>GW: GET /app/_api/weatherapi/weatherforecast Note over GW: Route match: /app/_api/weatherapi/{**catch-all} Note over GW: PathRemovePrefix: /app/_api/weatherapi Note over GW: Cluster: cluster-weatherapi GW->>API: GET /weatherforecast (via service discovery) API-->>GW: 200 OK [{"date":"...","temperatureC":25}] GW-->>WASM: 200 OK [{"date":"...","temperatureC":25}]Telemetry flow
sequenceDiagram participant WASM as Blazor WASM participant Proxy as Gateway / Server participant Dashboard as Aspire Dashboard Note over WASM: User clicks "Weather" WASM->>Proxy: POST /_otlp/v1/traces<br/>(protobuf, activity spans) Proxy->>Dashboard: Forward to OTLP endpoint<br/>(with auth headers) Dashboard-->>Proxy: 200 OK Proxy-->>WASM: 200 OK WASM->>Proxy: POST /_otlp/v1/logs<br/>(protobuf, structured logs) Proxy->>Dashboard: Forward to OTLP endpoint Dashboard-->>Proxy: 200 OK Proxy-->>WASM: 200 OKThe OTLP auth headers (
x-otlp-api-key) are included in the client configuration so the browser can authenticate with the dashboard's collector. The proxy passes these headers through transparently.YARP configuration (generated)
All YARP configuration is emitted as environment variables at orchestration time. The gateway/server reads them via
LoadFromConfig(configuration.GetSection("ReverseProxy")).Standalone (per-app, with prefix)
Hosted (no prefix)
Client configuration JSON
Both models deliver the same JSON structure to the WASM client. The only difference is how it gets there (HTTP endpoint vs. DOM comment):
{ "webAssembly": { "environment": { "services__weatherapi__https__0": "https://localhost:7101/_api/weatherapi", "services__weatherapi__http__0": "http://localhost:5101/_api/weatherapi", "OTEL_EXPORTER_OTLP_ENDPOINT": "https://localhost:7101/_otlp/", "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf", "OTEL_SERVICE_NAME": "app", "OTEL_EXPORTER_OTLP_HEADERS": "x-otlp-api-key=abc123" } } }Key detail: Service URLs point to the proxy, not to the backend service directly. For example,
services__weatherapi__https__0resolves tohttps://localhost:7101/_api/weatherapi(the proxy's address), not to weatherapi's actual endpoint. This is what makes same-origin proxying work — the WASM client'sHttpClientthinks it's talking to weatherapi, but it's actually talking to the proxy.WebAssembly telemetry
The client-side telemetry library adapts OpenTelemetry for the browser runtime, handling three platform-specific constraints.
Manual provider initialization
The OpenTelemetry SDK uses
IHostedServiceto start exporters. In WebAssembly, hosted services don't automatically run. The client must force-initialize the providers:OTLP/HTTP instead of gRPC
The standard OTLP exporter uses gRPC, which requires HTTP/2 — not available in browser fetch. The client-side telemetry library uses custom OTLP/HTTP exporters that send protobuf payloads via
HttpClient(browser fetch), which only requires HTTP/1.1.Direct
HttpClientfor exportersUsing
IHttpClientFactoryinside the OTLP exporter causes a reentrancy crash: the exporter is resolved from DI duringLazy<T>initialization, but it calls back into DI to get anHttpClient, which triggers the sameLazy<T>. The exporters usenew HttpClient()directly, bypassing the DI container.Component overview
graph TB subgraph Hosting["Aspire.Hosting.Blazor (AppHost side)"] BGE["BlazorGatewayExtensions<br/>AddBlazorGateway, WithClient"] BHE["BlazorHostedExtensions<br/>ProxyService, ProxyTelemetry"] GCB["GatewayConfigurationBuilder<br/>YARP routes + client config"] EMT["EndpointsManifestTransformer<br/>Static asset rewrites"] end subgraph ClientLib["ClientServiceDefaults (WASM side)"] CSE["Extensions.cs<br/>AddBlazorClientServiceDefaults"] JSI["lib.module.js<br/>JS Initializer"] OTL["Telemetry/<br/>OTLP/HTTP exporters"] end subgraph ServerLib["ServiceDefaults (Server side)"] SDE["Extensions.cs<br/>AddServiceDefaults"] end BGE --> GCB BHE --> GCB BGE --> EMT CSE --> OTL JSI --> CSE style Hosting fill:#e8f4fd style ClientLib fill:#fff3cd style ServerLib fill:#f0f0f0Summary comparison
AddBlazorWasmProject+AddBlazorGateway+WithClientProxyService+ProxyTelemetry/_blazor/_configurationHTTP endpoint<!--Blazor-Client-Config:BASE64-->DOM commentonRuntimeConfigLoaded(standalone boot)beforeWebAssemblyStart(hosted boot)/app/)app)blazorapp+blazorapp (client)Summary of the PR contents
Adds
Aspire.Hosting.Blazor— a hosting integration that brings Blazor WebAssembly apps into Aspire orchestration with service discovery, YARP-based API proxying, and OpenTelemetry forwarding.Hosting library —
src/Aspire.Hosting.Blazor/A new hosting integration that extends Aspire to Blazor WebAssembly apps:
Standalone model (WASM app with no server):
AddBlazorWasmProject()— registers a WASM project as an Aspire resourceAddBlazorGateway()— creates an auto-generated ASP.NET Core + YARP gateway processWithClient()— attaches a WASM app to the gateway under a path prefixdotnet run --filescriptHosted model (Blazor Web App with WASM client components):
ProxyService()— adds a YARP route so the WASM client can call a backend service through the hostProxyTelemetry()— adds a YARP route for OTLP traffic from the browser to the Aspire dashboard<!--Blazor-Client-Config:BASE64-->) embedded during prerenderingShared infrastructure:
GatewayConfigurationBuilder— emits YARP route/cluster config and client config as environment variablesEndpointsManifestTransformer— rewritesstaticwebassets.endpoints.jsonto add path prefixes and SPA fallback routesapiPrefix/otlpPrefixroute segments (defaults:_api,_otlp)Tests —
tests/Aspire.Hosting.Blazor.Tests/38 unit tests covering:
AddBlazorWasmAppTests— resource registration, project path resolutionGatewayConfigurationBuilderTests— YARP route/cluster generation, client config JSON, custom prefixes, OTLP proxy configEndpointsManifestTransformerTests— manifest rewriting, prefix injection, SPA fallbackWithBlazorAppTests— gateway annotation management, service reference forwardingBlazorHostedExtensionsTests—ProxyService/ProxyTelemetryenv var emission, annotation trackingPlaygrounds
playground/AspireWithBlazorStandalone/— standalone WASM + gateway + weather APIAddBlazorWasmProject+AddBlazorGateway+WithClientClientServiceDefaultswith custom OTLP/HTTP protobuf exporters for WASM/_blazor/_configurationendpointplayground/AspireWithBlazorHosted/— Blazor Web App with interactive WASM client componentsProxyService+ProxyTelemetryBlazorClientConfiguration.razorcomponent embeds config in DOM commentbeforeWebAssemblyStartBoth playgrounds include READMEs with Mermaid architecture diagrams.
Client-side telemetry (
ClientServiceDefaults)Each playground includes a
ClientServiceDefaultsproject with:HttpClient(browser fetch) over HTTP/1.1TaskBasedBatchExportProcessor— replaces the defaultThread-based batch processor that isn't compatible with WebAssembly's single-threaded runtimeAddBlazorClientServiceDefaults()— configures OpenTelemetry, service discovery, and resilience for the WASM clientonRuntimeConfigLoaded(standalone) /beforeWebAssemblyStart(hosted) injects environment variables into the WASM runtimeApproach: same-origin reverse proxy
All browser traffic routes through a YARP reverse proxy on the same origin as the WASM app:
/_api/{service}/*→ proxied via YARP to the backend service/_otlp/*→ proxied to the Aspire dashboard's OTLP collectorThis eliminates CORS entirely — the browser only talks to its own origin. Backend services and the OTLP collector remain on the internal network, and auth tokens never reach the browser.
What won't be needed in .NET 11
Several workarounds in this PR exist because .NET 10 lacks certain capabilities. Work already in
dotnet/aspnetcoreanddotnet/sdkfor .NET 11 will eliminate them:Environment variables → IConfiguration (dotnet/aspnetcore#64578)
Currently, the WASM client must manually call
builder.Configuration.AddEnvironmentVariables()to bridge env vars intoIConfigurationfor service discovery. In .NET 11,WebAssemblyHostBuilder.CreateDefault()does this automatically. The playgroundProgram.csfiles will no longer need this call.Framework-provided Blazor Gateway (dotnet/aspnetcore
blazor-gatewaybranch)The auto-generated
Gateway.csscript in this PR (Scripts/Gateway.cs) will be replaced byMicrosoft.AspNetCore.Components.Gateway— a framework-provided gateway process that readsClientAppsconfig, supports YARP with service discovery, and replaces the oldWasmDevServer. TheAspire.Hosting.Blazorintegration will launch the framework gateway instead of generating its own.SPA fallback via MSBuild (dotnet/sdk
fallback-endpointsbranch)The
EndpointsManifestTransformerin this PR writes C# code to create SPA fallback routes ({**fallback:nonfile}catch-all forindex.html). In .NET 11, settingStaticWebAssetSpaFallbackEnabled=truein the project file handles this declaratively — the endpoints manifest includes the fallback automatically, and the transformer code can be removed.Blazor component metrics and tracing (dotnet/aspnetcore#61609, #64737)
The
ClientServiceDefaultstelemetry currently only capturesHttpClientinstrumentation from the WASM client. .NET 11 addsComponentsMetricsandComponentsActivitySourcewith instruments for component rendering, navigation, event handling, and render diffs — all emittable from WASM when theSystem.Diagnostics.Metrics.Meter.IsSupportedfeature switch is enabled. These will show up in the Aspire dashboard automatically.JS initializer URL fix (dotnet/aspnetcore#63185)
The gateway serves WASM apps under path prefixes (e.g.,
/store/). An aspnetcore fix ensures JS initializer module URLs are computed correctly withnew URL(path, document.baseURI)instead of string concatenation, preventing load failures through the gateway.