Skip to content

[Blazor] Flow the WebAssembly options from Server to client through SSR marker #60714

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Mar 10, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
services.TryAddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
services.AddSupplyValueFromQueryProvider();
services.TryAddCascadingValue(sp => sp.GetRequiredService<EndpointHtmlRenderer>().HttpContext);
services.TryAddScoped<WebAssemblySettingsEmitter>();

services.TryAddScoped<ResourceCollectionProvider>();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Hosting;

namespace Microsoft.AspNetCore.Components.Endpoints;

internal record WebAssemblySettings(string EnvironmentName, Dictionary<string, string> EnvironmentVariables);

internal class WebAssemblySettingsEmitter(IHostEnvironment hostEnvironment)
{
private bool wasEmittedAlready;

private const string dotnetModifiableAssembliesName = "DOTNET_MODIFIABLE_ASSEMBLIES";
private const string aspnetcoreBrowserToolsName = "__ASPNETCORE_BROWSER_TOOLS";

private static readonly string? s_dotnetModifiableAssemblies = GetNonEmptyEnvironmentVariableValue(dotnetModifiableAssembliesName);
private static readonly string? s_aspnetcoreBrowserTools = GetNonEmptyEnvironmentVariableValue(aspnetcoreBrowserToolsName);

private static string? GetNonEmptyEnvironmentVariableValue(string name)
=> Environment.GetEnvironmentVariable(name) is { Length: > 0 } value ? value : null;

public bool TryGetSettingsOnce([NotNullWhen(true)] out WebAssemblySettings? settings)
{
if (wasEmittedAlready)
{
settings = default;
return false;
}

var environmentVariables = new Dictionary<string, string>();

// DOTNET_MODIFIABLE_ASSEMBLIES is used by the runtime to initialize hot-reload specific environment variables and is configured
// by the launching process (dotnet-watch / Visual Studio).
// Always add the header if the environment variable is set, regardless of the kind of environment.
if (s_dotnetModifiableAssemblies != null)
{
environmentVariables[dotnetModifiableAssembliesName] = s_dotnetModifiableAssemblies;
}

// See https://github.com/dotnet/aspnetcore/issues/37357#issuecomment-941237000
// Translate the _ASPNETCORE_BROWSER_TOOLS environment configured by the browser tools agent in to a HTTP response header.
if (s_aspnetcoreBrowserTools != null)
{
environmentVariables[aspnetcoreBrowserToolsName] = s_aspnetcoreBrowserTools;
}

wasEmittedAlready = true;
settings = new (hostEnvironment.EnvironmentName, environmentVariables);
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,15 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo
_httpContext.Response.Headers.CacheControl = "no-cache, no-store, max-age=0";
}

if (marker.Type is ComponentMarker.WebAssemblyMarkerType or ComponentMarker.AutoMarkerType)
{
if (_httpContext.RequestServices.GetRequiredService<WebAssemblySettingsEmitter>().TryGetSettingsOnce(out var settings))
{
var settingsJson = JsonSerializer.Serialize(settings, ServerComponentSerializationSettings.JsonSerializationOptions);
output.Write($"<!--Blazor-WebAssembly:{settingsJson}-->");
}
}

var serializedStartRecord = JsonSerializer.Serialize(marker, ServerComponentSerializationSettings.JsonSerializationOptions);
output.Write("<!--Blazor:");
output.Write(serializedStartRecord);
Expand Down
18 changes: 12 additions & 6 deletions src/Components/Web.JS/src/Boot.WebAssembly.Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRen
import { Pointer } from './Platform/Platform';
import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
import { addDispatchEventMiddleware } from './Rendering/WebRendererInteropMethods';
import { WebAssemblyComponentDescriptor, discoverWebAssemblyPersistedState } from './Services/ComponentDescriptorDiscovery';
import { WebAssemblyComponentDescriptor, WebAssemblyServerOptions, discoverWebAssemblyPersistedState } from './Services/ComponentDescriptorDiscovery';
import { receiveDotNetDataStream } from './StreamingInterop';
import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher';
import { MonoConfig } from '@microsoft/dotnet-runtime';
Expand Down Expand Up @@ -68,23 +68,23 @@ export function setWebAssemblyOptions(initializersReady: Promise<Partial<WebAsse
}
}

export function startWebAssembly(components: RootComponentManager<WebAssemblyComponentDescriptor>): Promise<void> {
export function startWebAssembly(components: RootComponentManager<WebAssemblyComponentDescriptor>, options: WebAssemblyServerOptions | undefined): Promise<void> {
if (startPromise !== undefined) {
throw new Error('Blazor WebAssembly has already started.');
}

startPromise = new Promise(startCore.bind(null, components));
startPromise = new Promise(startCore.bind(null, components, options));

return startPromise;
}

async function startCore(components: RootComponentManager<WebAssemblyComponentDescriptor>, resolve, _) {
async function startCore(components: RootComponentManager<WebAssemblyComponentDescriptor>, options: WebAssemblyServerOptions | undefined, resolve, _) {
if (inAuthRedirectIframe()) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
await new Promise(() => { }); // See inAuthRedirectIframe for explanation
}

const platformLoadPromise = loadWebAssemblyPlatformIfNotStarted();
const platformLoadPromise = loadWebAssemblyPlatformIfNotStarted(options);

addDispatchEventMiddleware((browserRendererId, eventHandlerId, continuation) => {
// It's extremely unusual, but an event can be raised while we're in the middle of synchronously applying a
Expand Down Expand Up @@ -206,13 +206,19 @@ export function waitForBootConfigLoaded(): Promise<MonoConfig> {
return bootConfigPromise;
}

export function loadWebAssemblyPlatformIfNotStarted(): Promise<void> {
export function loadWebAssemblyPlatformIfNotStarted(serverOptions: WebAssemblyServerOptions | undefined): Promise<void> {
platformLoadPromise ??= (async () => {
await initializersPromise;
const finalOptions = options ?? {};
if (!finalOptions.environment) {
finalOptions.environment = serverOptions?.environmentName ?? undefined;
}
const existingConfig = options?.configureRuntime;
finalOptions.configureRuntime = (config) => {
existingConfig?.(config);
if (serverOptions?.environmentVariables) {
config.withEnvironmentVariables(serverOptions.environmentVariables);
}
if (waitForRootComponents) {
config.withEnvironmentVariable('__BLAZOR_WEBASSEMBLY_WAIT_FOR_ROOT_COMPONENTS', 'true');
}
Expand Down
6 changes: 4 additions & 2 deletions src/Components/Web.JS/src/Boot.WebAssembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Blazor } from './GlobalExports';
import { shouldAutoStart } from './BootCommon';
import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
import { setWebAssemblyOptions, startWebAssembly } from './Boot.WebAssembly.Common';
import { WebAssemblyComponentDescriptor, discoverComponents } from './Services/ComponentDescriptorDiscovery';
import { WebAssemblyComponentDescriptor, discoverComponents, discoverWebAssemblyOptions } from './Services/ComponentDescriptorDiscovery';
import { DotNet } from '@microsoft/dotnet-js-interop';
import { InitialRootComponentsList } from './Services/InitialRootComponentsList';
import { JSEventRegistry } from './Services/JSEventRegistry';
Expand All @@ -24,8 +24,10 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {

JSEventRegistry.create(Blazor);
const webAssemblyComponents = discoverComponents(document, 'webassembly') as WebAssemblyComponentDescriptor[];
const webAssemblyOptions = discoverWebAssemblyOptions(document);

const components = new InitialRootComponentsList(webAssemblyComponents);
await startWebAssembly(components);
await startWebAssembly(components, webAssemblyOptions);
}

Blazor.start = boot;
Expand Down
7 changes: 5 additions & 2 deletions src/Components/Web.JS/src/Rendering/DomMerging/DomSync.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

import { AutoComponentDescriptor, ComponentDescriptor, ServerComponentDescriptor, WebAssemblyComponentDescriptor, canMergeDescriptors, discoverComponents, mergeDescriptors } from '../../Services/ComponentDescriptorDiscovery';
import { AutoComponentDescriptor, ComponentDescriptor, ServerComponentDescriptor, WebAssemblyComponentDescriptor, WebAssemblyServerOptions, canMergeDescriptors, discoverComponents, discoverWebAssemblyOptions, mergeDescriptors } from '../../Services/ComponentDescriptorDiscovery';
import { isInteractiveRootComponentElement } from '../BrowserRenderer';
import { applyAnyDeferredValue } from '../DomSpecialPropertyUtil';
import { LogicalElement, getLogicalChildrenArray, getLogicalNextSibling, getLogicalParent, getLogicalRootDescriptor, insertLogicalChild, insertLogicalChildBefore, isLogicalElement, toLogicalElement, toLogicalRootCommentElement } from '../LogicalElements';
Expand All @@ -13,6 +13,7 @@ let descriptorHandler: DescriptorHandler | null = null;

export interface DescriptorHandler {
registerComponent(descriptor: ComponentDescriptor): void;
setWebAssemblyOptions(options: WebAssemblyServerOptions | undefined): void;
}

export function attachComponentDescriptorHandler(handler: DescriptorHandler) {
Expand All @@ -21,6 +22,8 @@ export function attachComponentDescriptorHandler(handler: DescriptorHandler) {

export function registerAllComponentDescriptors(root: Node) {
const descriptors = upgradeComponentCommentsToLogicalRootComments(root);
const webAssemblyOptions = discoverWebAssemblyOptions(root);
descriptorHandler?.setWebAssemblyOptions(webAssemblyOptions);

for (const descriptor of descriptors) {
descriptorHandler?.registerComponent(descriptor);
Expand Down Expand Up @@ -168,7 +171,7 @@ function treatAsMatch(destination: Node, source: Node) {
}

if (destinationRootDescriptor) {
// Update the existing descriptor with hte new descriptor's data
// Update the existing descriptor with the new descriptor's data
mergeDescriptors(destinationRootDescriptor, sourceRootDescriptor);

const isDestinationInteractive = isInteractiveRootComponentElement(destinationAsLogicalElement);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ export function discoverComponents(root: Node, type: 'webassembly' | 'server' |
const blazorServerStateCommentRegularExpression = /^\s*Blazor-Server-Component-State:(?<state>[a-zA-Z0-9+/=]+)$/;
const blazorWebAssemblyStateCommentRegularExpression = /^\s*Blazor-WebAssembly-Component-State:(?<state>[a-zA-Z0-9+/=]+)$/;
const blazorWebInitializerCommentRegularExpression = /^\s*Blazor-Web-Initializers:(?<initializers>[a-zA-Z0-9+/=]+)$/;
const blazorWebAssemblyOptionsCommentRegularExpression = /^\s*Blazor-WebAssembly:[^{]*(?<options>.*)$/;

export function discoverWebAssemblyOptions(root: Node): WebAssemblyServerOptions | undefined {
const optionsJson = discoverBlazorComment(root, blazorWebAssemblyOptionsCommentRegularExpression, 'options');
if (!optionsJson) {
return undefined;
}
const options = JSON.parse(optionsJson);
return options;
}

export function discoverServerPersistedState(node: Node): string | null | undefined {
return discoverBlazorComment(node, blazorServerStateCommentRegularExpression);
Expand Down Expand Up @@ -339,6 +349,11 @@ export type ServerComponentDescriptor = ServerComponentMarker & DescriptorData;
export type WebAssemblyComponentDescriptor = WebAssemblyComponentMarker & DescriptorData;
export type AutoComponentDescriptor = AutoComponentMarker & DescriptorData;

export type WebAssemblyServerOptions = {
environmentName: string,
environmentVariables: { [i: string]: string; }
};

type DescriptorData = {
uniqueId: number;
start: Comment;
Expand Down
12 changes: 9 additions & 3 deletions src/Components/Web.JS/src/Services/WebRootComponentManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

import { ComponentDescriptor, ComponentMarker, descriptorToMarker } from './ComponentDescriptorDiscovery';
import { ComponentDescriptor, ComponentMarker, descriptorToMarker, WebAssemblyServerOptions } from './ComponentDescriptorDiscovery';
import { isRendererAttached, registerRendererAttachedListener } from '../Rendering/WebRendererInteropMethods';
import { WebRendererId } from '../Rendering/WebRendererId';
import { DescriptorHandler } from '../Rendering/DomMerging/DomSync';
Expand Down Expand Up @@ -63,6 +63,8 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent

private _circuitInactivityTimeoutId: any;

private _webAssemblyOptions: WebAssemblyServerOptions | undefined;

// Implements RootComponentManager.
// An empty array becuase all root components managed
// by WebRootComponentManager are added and removed dynamically.
Expand Down Expand Up @@ -94,6 +96,10 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent
this.rootComponentsMayRequireRefresh();
}

public setWebAssemblyOptions(webAssemblyOptions: WebAssemblyServerOptions | undefined): void {
this._webAssemblyOptions = webAssemblyOptions;
}

public registerComponent(descriptor: ComponentDescriptor) {
if (this._seenDescriptors.has(descriptor)) {
return;
Expand Down Expand Up @@ -132,7 +138,7 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent

setWaitForRootComponents();

const loadWebAssemblyPromise = loadWebAssemblyPlatformIfNotStarted();
const loadWebAssemblyPromise = loadWebAssemblyPlatformIfNotStarted(this._webAssemblyOptions);
const bootConfig = await waitForBootConfigLoaded();

if (maxParallelDownloadsOverride !== undefined) {
Expand Down Expand Up @@ -182,7 +188,7 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent
this.startLoadingWebAssemblyIfNotStarted();

if (!hasStartedWebAssembly()) {
await startWebAssembly(this);
await startWebAssembly(this, this._webAssemblyOptions);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveWebAssembly
var descriptors = StaticAssetsEndpointDataSourceHelper.ResolveStaticAssetDescriptors(endpointBuilder, options.StaticAssetsManifestPath);
if (descriptors != null && descriptors.Count > 0)
{
ComponentWebAssemblyConventions.AddBlazorWebAssemblyConventions(descriptors, environment);
return builder;
}

Expand Down

This file was deleted.

Loading