Skip to content

Commit 5762f36

Browse files
authored
[Blazor] Flow the WebAssembly options from Server to client through SSR marker (#60714)
1 parent 12d57dd commit 5762f36

11 files changed

+127
-66
lines changed

src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
7171
services.TryAddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
7272
services.AddSupplyValueFromQueryProvider();
7373
services.TryAddCascadingValue(sp => sp.GetRequiredService<EndpointHtmlRenderer>().HttpContext);
74+
services.TryAddScoped<WebAssemblySettingsEmitter>();
7475

7576
services.TryAddScoped<ResourceCollectionProvider>();
7677

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
using Microsoft.Extensions.Hosting;
6+
7+
namespace Microsoft.AspNetCore.Components.Endpoints;
8+
9+
internal record WebAssemblySettings(string EnvironmentName, Dictionary<string, string> EnvironmentVariables);
10+
11+
internal class WebAssemblySettingsEmitter(IHostEnvironment hostEnvironment)
12+
{
13+
private bool wasEmittedAlready;
14+
15+
private const string dotnetModifiableAssembliesName = "DOTNET_MODIFIABLE_ASSEMBLIES";
16+
private const string aspnetcoreBrowserToolsName = "__ASPNETCORE_BROWSER_TOOLS";
17+
18+
private static readonly string? s_dotnetModifiableAssemblies = GetNonEmptyEnvironmentVariableValue(dotnetModifiableAssembliesName);
19+
private static readonly string? s_aspnetcoreBrowserTools = GetNonEmptyEnvironmentVariableValue(aspnetcoreBrowserToolsName);
20+
21+
private static string? GetNonEmptyEnvironmentVariableValue(string name)
22+
=> Environment.GetEnvironmentVariable(name) is { Length: > 0 } value ? value : null;
23+
24+
public bool TryGetSettingsOnce([NotNullWhen(true)] out WebAssemblySettings? settings)
25+
{
26+
if (wasEmittedAlready)
27+
{
28+
settings = default;
29+
return false;
30+
}
31+
32+
var environmentVariables = new Dictionary<string, string>();
33+
34+
// DOTNET_MODIFIABLE_ASSEMBLIES is used by the runtime to initialize hot-reload specific environment variables and is configured
35+
// by the launching process (dotnet-watch / Visual Studio).
36+
// Always add the header if the environment variable is set, regardless of the kind of environment.
37+
if (s_dotnetModifiableAssemblies != null)
38+
{
39+
environmentVariables[dotnetModifiableAssembliesName] = s_dotnetModifiableAssemblies;
40+
}
41+
42+
// See https://github.com/dotnet/aspnetcore/issues/37357#issuecomment-941237000
43+
// Translate the _ASPNETCORE_BROWSER_TOOLS environment configured by the browser tools agent in to a HTTP response header.
44+
if (s_aspnetcoreBrowserTools != null)
45+
{
46+
environmentVariables[aspnetcoreBrowserToolsName] = s_aspnetcoreBrowserTools;
47+
}
48+
49+
wasEmittedAlready = true;
50+
settings = new (hostEnvironment.EnvironmentName, environmentVariables);
51+
return true;
52+
}
53+
}

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,15 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo
271271
_httpContext.Response.Headers.CacheControl = "no-cache, no-store, max-age=0";
272272
}
273273

274+
if (marker.Type is ComponentMarker.WebAssemblyMarkerType or ComponentMarker.AutoMarkerType)
275+
{
276+
if (_httpContext.RequestServices.GetRequiredService<WebAssemblySettingsEmitter>().TryGetSettingsOnce(out var settings))
277+
{
278+
var settingsJson = JsonSerializer.Serialize(settings, ServerComponentSerializationSettings.JsonSerializationOptions);
279+
output.Write($"<!--Blazor-WebAssembly:{settingsJson}-->");
280+
}
281+
}
282+
274283
var serializedStartRecord = JsonSerializer.Serialize(marker, ServerComponentSerializationSettings.JsonSerializationOptions);
275284
output.Write("<!--Blazor:");
276285
output.Write(serializedStartRecord);

src/Components/Endpoints/test/EndpointHtmlRendererTest.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class EndpointHtmlRendererTest
3434
{
3535
private const string MarkerPrefix = "<!--Blazor:";
3636
private const string PrerenderedComponentPattern = "^<!--Blazor:(?<preamble>.*?)-->(?<content>.+?)<!--Blazor:(?<epilogue>.*?)-->$";
37+
private const string WebAssemblyOptionsPattern = "^<!--Blazor-WebAssembly:(.*?)-->";
3738
private const string ComponentPattern = "^<!--Blazor:(.*?)-->$";
3839

3940
private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider();
@@ -57,6 +58,7 @@ public async Task CanRender_ParameterlessComponent_ClientMode()
5758
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new InteractiveWebAssemblyRenderMode(prerender: false), ParameterView.Empty);
5859
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
5960
var content = writer.ToString();
61+
content = AssertAndStripWebAssemblyOptions(content);
6062
var match = Regex.Match(content, ComponentPattern);
6163

6264
// Assert
@@ -80,6 +82,7 @@ public async Task CanPrerender_ParameterlessComponent_ClientMode()
8082
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), RenderMode.InteractiveWebAssembly, ParameterView.Empty);
8183
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
8284
var content = writer.ToString();
85+
content = AssertAndStripWebAssemblyOptions(content);
8386
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
8487

8588
// Assert
@@ -123,6 +126,7 @@ public async Task CanRender_ComponentWithParameters_ClientMode()
123126
}));
124127
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
125128
var content = writer.ToString();
129+
content = AssertAndStripWebAssemblyOptions(content);
126130
var match = Regex.Match(content, ComponentPattern);
127131

128132
// Assert
@@ -160,6 +164,7 @@ public async Task CanRender_ComponentWithNullParameters_ClientMode()
160164
}));
161165
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
162166
var content = writer.ToString();
167+
content = AssertAndStripWebAssemblyOptions(content);
163168
var match = Regex.Match(content, ComponentPattern);
164169

165170
// Assert
@@ -195,6 +200,7 @@ public async Task CanPrerender_ComponentWithParameters_ClientMode()
195200
}));
196201
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
197202
var content = writer.ToString();
203+
content = AssertAndStripWebAssemblyOptions(content);
198204
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
199205

200206
// Assert
@@ -244,6 +250,7 @@ public async Task CanPrerender_ComponentWithNullParameters_ClientMode()
244250
}));
245251
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
246252
var content = writer.ToString();
253+
content = AssertAndStripWebAssemblyOptions(content);
247254
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);
248255

249256
// Assert
@@ -1063,6 +1070,7 @@ public async Task RenderMode_CanRenderInteractiveComponents()
10631070
var lines = content.Replace("\r\n", "\n").Split('\n');
10641071
var serverMarkerMatch = Regex.Match(lines[0], PrerenderedComponentPattern);
10651072
var serverNonPrerenderedMarkerMatch = Regex.Match(lines[1], ComponentPattern);
1073+
lines[2] = AssertAndStripWebAssemblyOptions(lines[2]);
10661074
var webAssemblyMarkerMatch = Regex.Match(lines[2], PrerenderedComponentPattern);
10671075
var webAssemblyNonPrerenderedMarkerMatch = Regex.Match(lines[3], ComponentPattern);
10681076

@@ -1167,6 +1175,8 @@ public async Task DoesNotEmitNestedRenderModeBoundaries()
11671175
var numMarkers = Regex.Matches(content, MarkerPrefix).Count;
11681176
Assert.Equal(2, numMarkers); // A start and an end marker
11691177

1178+
content = AssertAndStripWebAssemblyOptions(content);
1179+
11701180
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Singleline);
11711181
Assert.True(match.Success);
11721182
var preamble = match.Groups["preamble"].Value;
@@ -1498,6 +1508,14 @@ await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(
14981508
}
14991509
}
15001510

1511+
private string AssertAndStripWebAssemblyOptions(string content)
1512+
{
1513+
var wasmOptionsMatch = Regex.Match(content, WebAssemblyOptionsPattern);
1514+
Assert.True(wasmOptionsMatch.Success);
1515+
content = content.Substring(wasmOptionsMatch.Groups[0].Length);
1516+
return content;
1517+
}
1518+
15011519
private class NamedEventHandlerComponent : ComponentBase
15021520
{
15031521
[Parameter]
@@ -1681,6 +1699,7 @@ private static ServiceCollection CreateDefaultServiceCollection()
16811699
services.AddSingleton<AntiforgeryStateProvider, EndpointAntiforgeryStateProvider>();
16821700
services.AddSingleton<ICascadingValueSupplier>(_ => new SupplyParameterFromFormValueProvider(null, ""));
16831701
services.AddScoped<ResourceCollectionProvider>();
1702+
services.AddSingleton(new WebAssemblySettingsEmitter(new TestEnvironment(Environments.Development)));
16841703
return services;
16851704
}
16861705

src/Components/Web.JS/src/Boot.WebAssembly.Common.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRen
1111
import { Pointer } from './Platform/Platform';
1212
import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
1313
import { addDispatchEventMiddleware } from './Rendering/WebRendererInteropMethods';
14-
import { WebAssemblyComponentDescriptor, discoverWebAssemblyPersistedState } from './Services/ComponentDescriptorDiscovery';
14+
import { WebAssemblyComponentDescriptor, WebAssemblyServerOptions, discoverWebAssemblyPersistedState } from './Services/ComponentDescriptorDiscovery';
1515
import { receiveDotNetDataStream } from './StreamingInterop';
1616
import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher';
1717
import { MonoConfig } from '@microsoft/dotnet-runtime';
@@ -68,23 +68,23 @@ export function setWebAssemblyOptions(initializersReady: Promise<Partial<WebAsse
6868
}
6969
}
7070

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

76-
startPromise = new Promise(startCore.bind(null, components));
76+
startPromise = new Promise(startCore.bind(null, components, options));
7777

7878
return startPromise;
7979
}
8080

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

87-
const platformLoadPromise = loadWebAssemblyPlatformIfNotStarted();
87+
const platformLoadPromise = loadWebAssemblyPlatformIfNotStarted(options);
8888

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

209-
export function loadWebAssemblyPlatformIfNotStarted(): Promise<void> {
209+
export function loadWebAssemblyPlatformIfNotStarted(serverOptions: WebAssemblyServerOptions | undefined): Promise<void> {
210210
platformLoadPromise ??= (async () => {
211211
await initializersPromise;
212212
const finalOptions = options ?? {};
213+
if (!finalOptions.environment) {
214+
finalOptions.environment = serverOptions?.environmentName ?? undefined;
215+
}
213216
const existingConfig = options?.configureRuntime;
214217
finalOptions.configureRuntime = (config) => {
215218
existingConfig?.(config);
219+
if (serverOptions?.environmentVariables) {
220+
config.withEnvironmentVariables(serverOptions.environmentVariables);
221+
}
216222
if (waitForRootComponents) {
217223
config.withEnvironmentVariable('__BLAZOR_WEBASSEMBLY_WAIT_FOR_ROOT_COMPONENTS', 'true');
218224
}

src/Components/Web.JS/src/Boot.WebAssembly.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Blazor } from './GlobalExports';
66
import { shouldAutoStart } from './BootCommon';
77
import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
88
import { setWebAssemblyOptions, startWebAssembly } from './Boot.WebAssembly.Common';
9-
import { WebAssemblyComponentDescriptor, discoverComponents } from './Services/ComponentDescriptorDiscovery';
9+
import { WebAssemblyComponentDescriptor, discoverComponents, discoverWebAssemblyOptions } from './Services/ComponentDescriptorDiscovery';
1010
import { DotNet } from '@microsoft/dotnet-js-interop';
1111
import { InitialRootComponentsList } from './Services/InitialRootComponentsList';
1212
import { JSEventRegistry } from './Services/JSEventRegistry';
@@ -24,8 +24,10 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
2424

2525
JSEventRegistry.create(Blazor);
2626
const webAssemblyComponents = discoverComponents(document, 'webassembly') as WebAssemblyComponentDescriptor[];
27+
const webAssemblyOptions = discoverWebAssemblyOptions(document);
28+
2729
const components = new InitialRootComponentsList(webAssemblyComponents);
28-
await startWebAssembly(components);
30+
await startWebAssembly(components, webAssemblyOptions);
2931
}
3032

3133
Blazor.start = boot;

src/Components/Web.JS/src/Rendering/DomMerging/DomSync.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

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

1414
export interface DescriptorHandler {
1515
registerComponent(descriptor: ComponentDescriptor): void;
16+
setWebAssemblyOptions(options: WebAssemblyServerOptions | undefined): void;
1617
}
1718

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

2223
export function registerAllComponentDescriptors(root: Node) {
2324
const descriptors = upgradeComponentCommentsToLogicalRootComments(root);
25+
const webAssemblyOptions = discoverWebAssemblyOptions(root);
26+
descriptorHandler?.setWebAssemblyOptions(webAssemblyOptions);
2427

2528
for (const descriptor of descriptors) {
2629
descriptorHandler?.registerComponent(descriptor);
@@ -168,7 +171,7 @@ function treatAsMatch(destination: Node, source: Node) {
168171
}
169172

170173
if (destinationRootDescriptor) {
171-
// Update the existing descriptor with hte new descriptor's data
174+
// Update the existing descriptor with the new descriptor's data
172175
mergeDescriptors(destinationRootDescriptor, sourceRootDescriptor);
173176

174177
const isDestinationInteractive = isInteractiveRootComponentElement(destinationAsLogicalElement);

src/Components/Web.JS/src/Services/ComponentDescriptorDiscovery.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ export function discoverComponents(root: Node, type: 'webassembly' | 'server' |
1515
const blazorServerStateCommentRegularExpression = /^\s*Blazor-Server-Component-State:(?<state>[a-zA-Z0-9+/=]+)$/;
1616
const blazorWebAssemblyStateCommentRegularExpression = /^\s*Blazor-WebAssembly-Component-State:(?<state>[a-zA-Z0-9+/=]+)$/;
1717
const blazorWebInitializerCommentRegularExpression = /^\s*Blazor-Web-Initializers:(?<initializers>[a-zA-Z0-9+/=]+)$/;
18+
const blazorWebAssemblyOptionsCommentRegularExpression = /^\s*Blazor-WebAssembly:[^{]*(?<options>.*)$/;
19+
20+
export function discoverWebAssemblyOptions(root: Node): WebAssemblyServerOptions | undefined {
21+
const optionsJson = discoverBlazorComment(root, blazorWebAssemblyOptionsCommentRegularExpression, 'options');
22+
if (!optionsJson) {
23+
return undefined;
24+
}
25+
const options = JSON.parse(optionsJson);
26+
return options;
27+
}
1828

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

352+
export type WebAssemblyServerOptions = {
353+
environmentName: string,
354+
environmentVariables: { [i: string]: string; }
355+
};
356+
342357
type DescriptorData = {
343358
uniqueId: number;
344359
start: Comment;

src/Components/Web.JS/src/Services/WebRootComponentManager.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

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

6464
private _circuitInactivityTimeoutId: any;
6565

66+
private _webAssemblyOptions: WebAssemblyServerOptions | undefined;
67+
6668
// Implements RootComponentManager.
6769
// An empty array becuase all root components managed
6870
// by WebRootComponentManager are added and removed dynamically.
@@ -94,6 +96,10 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent
9496
this.rootComponentsMayRequireRefresh();
9597
}
9698

99+
public setWebAssemblyOptions(webAssemblyOptions: WebAssemblyServerOptions | undefined): void {
100+
this._webAssemblyOptions = webAssemblyOptions;
101+
}
102+
97103
public registerComponent(descriptor: ComponentDescriptor) {
98104
if (this._seenDescriptors.has(descriptor)) {
99105
return;
@@ -132,7 +138,7 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent
132138

133139
setWaitForRootComponents();
134140

135-
const loadWebAssemblyPromise = loadWebAssemblyPlatformIfNotStarted();
141+
const loadWebAssemblyPromise = loadWebAssemblyPlatformIfNotStarted(this._webAssemblyOptions);
136142
const bootConfig = await waitForBootConfigLoaded();
137143

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

184190
if (!hasStartedWebAssembly()) {
185-
await startWebAssembly(this);
191+
await startWebAssembly(this, this._webAssemblyOptions);
186192
}
187193
}
188194

src/Components/WebAssembly/Server/src/Builder/WebAssemblyRazorComponentsEndpointConventionBuilderExtensions.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveWebAssembly
5959
var descriptors = StaticAssetsEndpointDataSourceHelper.ResolveStaticAssetDescriptors(endpointBuilder, options.StaticAssetsManifestPath);
6060
if (descriptors != null && descriptors.Count > 0)
6161
{
62-
ComponentWebAssemblyConventions.AddBlazorWebAssemblyConventions(descriptors, environment);
6362
return builder;
6463
}
6564

0 commit comments

Comments
 (0)