Skip to content

Add Aspire.Hosting.Blazor integration for Blazor WebAssembly apps#15691

Open
javiercn wants to merge 25 commits intomicrosoft:mainfrom
javiercn:javiercn/aspire-blazor-host
Open

Add Aspire.Hosting.Blazor integration for Blazor WebAssembly apps#15691
javiercn wants to merge 25 commits intomicrosoft:mainfrom
javiercn:javiercn/aspire-blazor-host

Conversation

@javiercn
Copy link
Copy Markdown

@javiercn javiercn commented Mar 29, 2026

Overview

Aspire.Hosting.Blazor extends 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:

  • Discover and call backend services using standard Aspire service discovery (https+http://weatherapi)
  • Send OpenTelemetry data (traces, logs, metrics) to the Aspire dashboard
  • Appear as a first-class resource in the dashboard with its own identity and health status

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:

  • No CORS configuration — the browser talks only to its own origin, so no Access-Control-Allow-Origin headers are needed on backend services or the OTLP collector
  • No exposed internals — backend services and the dashboard's OTLP collector remain on the internal network; only the proxy is publicly reachable
  • No credential leakage — API keys and OTLP auth tokens stay on the server side and are never sent to the browser directly
  • No preflight overhead — same-origin requests skip the OPTIONS round-trip that CORS requires
graph 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:#f0f0f0
Loading

The proxy is either:

  • An auto-generated gateway — for standalone WASM apps that have no server
  • The existing Blazor Server host — for hosted apps where the server already exists

Two hosting models

Aspire.Hosting.Blazor supports 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.

var weatherApi = builder.AddProject<Projects.WeatherApi>("weatherapi");

var blazorApp = builder.AddBlazorWasmProject("app", "path/to/App.csproj")
    .WithReference(weatherApi);

var gateway = builder.AddBlazorGateway("gateway")
    .WithExternalHttpEndpoints()
    .WithClient(blazorApp);

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.

var weatherApi = builder.AddProject<Projects.WeatherApi>("weatherapi");

builder.AddProject<Projects.BlazorHost>("blazorapp")
    .ProxyService(weatherApi)    // WASM client can reach weatherapi via /_api/weatherapi/*
    .ProxyTelemetry();           // WASM client OTLP flows through /_otlp/*

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" --> Dashboard
Loading

The gateway is a real ASP.NET Core process launched by the AppHost. Its Program.cs is auto-generated as a dotnet run --file script 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's index.html must 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" --> Dashboard
Loading

No 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.cs loads them with LoadFromConfig.

Design details

Same-origin proxy vs. CORS

The proxy approach was chosen over CORS for several reasons:

Concern CORS approach Proxy approach
Configuration Every backend service needs AllowOrigin headers Zero CORS configuration
OTLP collector Dashboard must accept browser origins Dashboard stays internal
Credentials API keys leak to the browser Keys stay on the server
Preflight requests Every cross-origin request adds an OPTIONS round-trip No preflights (same origin)
Security surface Backend services exposed to internet Only the proxy is exposed

Environment 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 via IConfiguration
  • OTEL_EXPORTER_OTLP_ENDPOINT → OpenTelemetry SDK auto-configuration
  • OTEL_SERVICE_NAME → resource identification in the dashboard

By 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 into IConfiguration, and service discovery resolves https+http://weatherapi as 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/_configuration JSON 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 beforeWebAssemblyStart fires 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 config
Loading

Explicit 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.

builder.AddProject<Projects.BlazorHost>("blazorapp")
    .WithReference(weatherApi)       // server can call weatherapi (server-to-server)
    .WithReference(database)         // server can access the database
    .ProxyService(weatherApi)        // WASM client can also reach weatherapi (browser → YARP → API)
    // database is NOT proxied — the browser cannot reach it
    .ProxyTelemetry();

Configurable route prefixes

The default prefixes /_api and /_otlp are chosen to minimize collision with application routes. However, if an app has routes starting with /_api/, they can be customized:

// Standalone
gateway.WithClient(blazorApp, apiPrefix: "api-proxy", otlpPrefix: "telemetry");

// Hosted
builder.AddProject<Projects.BlazorHost>("blazorapp")
    .ProxyService(weatherApi, apiPrefix: "api-proxy")
    .ProxyTelemetry(otlpPrefix: "telemetry");

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 the OTEL_SERVICE_NAME for the WASM client:

  • blazorapp → server-side traces and logs
  • blazorapp (client) → browser-side traces and logs

This 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)" --> Proxy
Loading

API 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}]
Loading

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 OK
Loading

The 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)

# API route — proxies /app/_api/weatherapi/* → weatherapi service
ReverseProxy__Routes__route-app-weatherapi__ClusterId=cluster-weatherapi
ReverseProxy__Routes__route-app-weatherapi__Match__Path=/app/_api/weatherapi/{**catch-all}
ReverseProxy__Routes__route-app-weatherapi__Transforms__0__PathRemovePrefix=/app/_api/weatherapi
ReverseProxy__Clusters__cluster-weatherapi__Destinations__d1__Address=https+http://weatherapi

# OTLP route — proxies /app/_otlp/* → Aspire dashboard OTLP endpoint
ReverseProxy__Routes__route-otlp-app__ClusterId=cluster-otlp-dashboard
ReverseProxy__Routes__route-otlp-app__Match__Path=/app/_otlp/{**catch-all}
ReverseProxy__Routes__route-otlp-app__Transforms__0__PathRemovePrefix=/app/_otlp
ReverseProxy__Clusters__cluster-otlp-dashboard__Destinations__d1__Address={OTLP_ENDPOINT}

Hosted (no prefix)

# API route — proxies /_api/weatherapi/* → weatherapi service
ReverseProxy__Routes__route-weatherapi__ClusterId=cluster-weatherapi
ReverseProxy__Routes__route-weatherapi__Match__Path=/_api/weatherapi/{**catch-all}
ReverseProxy__Routes__route-weatherapi__Transforms__0__PathRemovePrefix=/_api/weatherapi
ReverseProxy__Clusters__cluster-weatherapi__Destinations__d1__Address=https+http://weatherapi

# OTLP route — proxies /_otlp/* → Aspire dashboard OTLP endpoint
ReverseProxy__Routes__route-otlp__ClusterId=cluster-otlp-dashboard
ReverseProxy__Routes__route-otlp__Match__Path=/_otlp/{**catch-all}
ReverseProxy__Routes__route-otlp__Transforms__0__PathRemovePrefix=/_otlp
ReverseProxy__Clusters__cluster-otlp-dashboard__Destinations__d1__Address={OTLP_ENDPOINT}

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__0 resolves to https://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's HttpClient thinks 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 IHostedService to start exporters. In WebAssembly, hosted services don't automatically run. The client must force-initialize the providers:

var host = builder.Build();
_ = host.Services.GetService<MeterProvider>();
_ = host.Services.GetService<TracerProvider>();
await host.RunAsync();

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 HttpClient for exporters

Using IHttpClientFactory inside the OTLP exporter causes a reentrancy crash: the exporter is resolved from DI during Lazy<T> initialization, but it calls back into DI to get an HttpClient, which triggers the same Lazy<T>. The exporters use new 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:#f0f0f0
Loading

Summary comparison

Standalone Hosted
AppHost API AddBlazorWasmProject + AddBlazorGateway + WithClient ProxyService + ProxyTelemetry
Proxy Auto-generated gateway process Existing Blazor Server
Static files Gateway serves them (from build output) Server serves them (built-in)
Config delivery /_blazor/_configuration HTTP endpoint <!--Blazor-Client-Config:BASE64--> DOM comment
JS hook onRuntimeConfigLoaded (standalone boot) beforeWebAssemblyStart (hosted boot)
Path prefix Per-app prefix (e.g., /app/) No prefix
Multi-app Yes — multiple WASM apps per gateway N/A — one client per server
Prerendering Not applicable Supported
Dashboard identity Separate resource (e.g., app) blazorapp + blazorapp (client)
CORS Not needed (same-origin proxy) Not needed (same-origin proxy)

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 resource
  • AddBlazorGateway() — creates an auto-generated ASP.NET Core + YARP gateway process
  • WithClient() — attaches a WASM app to the gateway under a path prefix
  • The gateway serves WASM static files, proxies API traffic, and forwards OTLP telemetry
  • The gateway process is auto-generated as a dotnet run --file script

Hosted model (Blazor Web App with WASM client components):

  • ProxyService() — adds a YARP route so the WASM client can call a backend service through the host
  • ProxyTelemetry() — adds a YARP route for OTLP traffic from the browser to the Aspire dashboard
  • Client configuration delivered via DOM comment (<!--Blazor-Client-Config:BASE64-->) embedded during prerendering

Shared infrastructure:

  • GatewayConfigurationBuilder — emits YARP route/cluster config and client config as environment variables
  • EndpointsManifestTransformer — rewrites staticwebassets.endpoints.json to add path prefixes and SPA fallback routes
  • Configurable apiPrefix/otlpPrefix route segments (defaults: _api, _otlp)
  • Publish mode support for Azure Container Apps

Tests — tests/Aspire.Hosting.Blazor.Tests/

38 unit tests covering:

  • AddBlazorWasmAppTests — resource registration, project path resolution
  • GatewayConfigurationBuilderTests — YARP route/cluster generation, client config JSON, custom prefixes, OTLP proxy config
  • EndpointsManifestTransformerTests — manifest rewriting, prefix injection, SPA fallback
  • WithBlazorAppTests — gateway annotation management, service reference forwarding
  • BlazorHostedExtensionsTestsProxyService/ProxyTelemetry env var emission, annotation tracking

Playgrounds

playground/AspireWithBlazorStandalone/ — standalone WASM + gateway + weather API

  • Demonstrates AddBlazorWasmProject + AddBlazorGateway + WithClient
  • Includes ClientServiceDefaults with custom OTLP/HTTP protobuf exporters for WASM
  • JS initializer fetches config from /_blazor/_configuration endpoint

playground/AspireWithBlazorHosted/ — Blazor Web App with interactive WASM client components

  • Demonstrates ProxyService + ProxyTelemetry
  • BlazorClientConfiguration.razor component embeds config in DOM comment
  • JS initializer reads config from DOM comment in beforeWebAssemblyStart

Both playgrounds include READMEs with Mermaid architecture diagrams.

Client-side telemetry (ClientServiceDefaults)

Each playground includes a ClientServiceDefaults project with:

  • Custom OTLP/HTTP exporters — the standard OpenTelemetry gRPC exporter doesn't work in the browser (no HTTP/2). These exporters serialize traces, logs, and metrics as protobuf and send via HttpClient (browser fetch) over HTTP/1.1
  • TaskBasedBatchExportProcessor — replaces the default Thread-based batch processor that isn't compatible with WebAssembly's single-threaded runtime
  • AddBlazorClientServiceDefaults() — configures OpenTelemetry, service discovery, and resilience for the WASM client
  • JS initializeronRuntimeConfigLoaded (standalone) / beforeWebAssemblyStart (hosted) injects environment variables into the WASM runtime

Approach: same-origin reverse proxy

All browser traffic routes through a YARP reverse proxy on the same origin as the WASM app:

  • API calls: /_api/{service}/* → proxied via YARP to the backend service
  • OTLP telemetry: /_otlp/* → proxied to the Aspire dashboard's OTLP collector

This 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/aspnetcore and dotnet/sdk for .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 into IConfiguration for service discovery. In .NET 11, WebAssemblyHostBuilder.CreateDefault() does this automatically. The playground Program.cs files will no longer need this call.

Framework-provided Blazor Gateway (dotnet/aspnetcore blazor-gateway branch)

The auto-generated Gateway.cs script in this PR (Scripts/Gateway.cs) will be replaced by Microsoft.AspNetCore.Components.Gateway — a framework-provided gateway process that reads ClientApps config, supports YARP with service discovery, and replaces the old WasmDevServer. The Aspire.Hosting.Blazor integration will launch the framework gateway instead of generating its own.

SPA fallback via MSBuild (dotnet/sdk fallback-endpoints branch)

The EndpointsManifestTransformer in this PR writes C# code to create SPA fallback routes ({**fallback:nonfile} catch-all for index.html). In .NET 11, setting StaticWebAssetSpaFallbackEnabled=true in 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 ClientServiceDefaults telemetry currently only captures HttpClient instrumentation from the WASM client. .NET 11 adds ComponentsMetrics and ComponentsActivitySource with instruments for component rendering, navigation, event handling, and render diffs — all emittable from WASM when the System.Diagnostics.Metrics.Meter.IsSupported feature 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 with new URL(path, document.baseURI) instead of string concatenation, preventing load failures through the gateway.

javiercn added 25 commits March 29, 2026 12:10
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
…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.
@github-actions
Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 15691

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 15691"

@javiercn javiercn marked this pull request as ready for review March 29, 2026 15:12
Copilot AI review requested due to automatic review settings March 29, 2026 15:12
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Blazor with resources/annotations, gateway + hosted extension APIs, manifest transformation, and supporting scripts.
  • Adds tests/Aspire.Hosting.Blazor.Tests validating 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.

@@ -0,0 +1,29 @@
var builder = DistributedApplication.CreateBuilder(args);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be called AppHost.cs

@davidfowl
Copy link
Copy Markdown
Contributor

Did you look at the existing yarp resource to see if we should layer on top for those scenarios?

@davidfowl davidfowl requested review from JamesNK and mitchdenny March 30, 2026 04:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants