Skip to content

Commit a5d9796

Browse files
[release/8.0-preview7] [Blazor] Support for determining the render mode dynamically (#49578)
# Support for determining the render mode dynamically Adds basic support for using the "Auto" render mode in Blazor apps. This allows the render mode for a component to resolve to either "Server" or "WebAssembly" depending on whether the WebAssembly runtime is downloaded and available on the client. Backport of #49477 ## Description In full-stack Blazor apps, components can be made interactive either via "WebAssembly" or "Server" render modes. The WebAssembly render mode has the advantage that it consumes fewer server resources, but the disadvantage that it delays "time to interactivity" because .NET WebAssembly runtime resources must be downloaded to the client first. Therefore, the "auto" render mode was introduced so that it: * Uses the WebAssembly render mode if the client already has the WebAssembly bits cached * Uses the Server render mode in other cases, while downloading WebAssembly resources in the background We also intend to add support for customizing the policy that determines which render mode "auto" mode should resolve to, but that's not included as part of this PR. Before this change, attempting to use the "auto" render mode would result in an error in the browser console and an unusable app. This PR allows customers to start experimenting with auto mode for their apps. Fixes #46397 ## Customer Impact Medium. The Auto render mode is already one of the three render modes customers can pick, but currently it just breaks the app. As we get closer to GA, we expect lots of customers to start trying out auto mode, so we'd like this feature to be made available as soon as possible so we can collect feedback and make the necessary corrections. ## Regression? - [ ] Yes - [X] No ## Risk - [ ] High - [ ] Medium - [X] Low This feature shouldn't impact functionality for existing Blazor Server/WebAssembly apps. It won't introduce a regression in the existing auto mode implementation, because the current implementation throws an exception. ## Verification - [X] Manual (required) - [ ] Automated
1 parent 0c0bc01 commit a5d9796

39 files changed

+706
-701
lines changed

src/Components/Endpoints/src/DependencyInjection/ServerComponentSerializer.cs

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,17 @@ namespace Microsoft.AspNetCore.Components.Endpoints;
99
// See the details of the component serialization protocol in ServerComponentDeserializer.cs on the Components solution.
1010
internal sealed class ServerComponentSerializer
1111
{
12-
public const int PreambleBufferSize = 3;
13-
1412
private readonly ITimeLimitedDataProtector _dataProtector;
1513

1614
public ServerComponentSerializer(IDataProtectionProvider dataProtectionProvider) =>
1715
_dataProtector = dataProtectionProvider
1816
.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
1917
.ToTimeLimitedDataProtector();
2018

21-
public ServerComponentMarker SerializeInvocation(ServerComponentInvocationSequence invocationId, Type type, ParameterView parameters, string key, bool prerendered)
19+
public void SerializeInvocation(ref ComponentMarker marker, ServerComponentInvocationSequence invocationId, Type type, ParameterView parameters)
2220
{
2321
var (sequence, serverComponent) = CreateSerializedServerComponent(invocationId, type, parameters);
24-
return prerendered ? ServerComponentMarker.Prerendered(sequence, serverComponent, key) : ServerComponentMarker.NonPrerendered(sequence, serverComponent, key);
22+
marker.WriteServerData(sequence, serverComponent);
2523
}
2624

2725
private (int sequence, string payload) CreateSerializedServerComponent(
@@ -45,29 +43,4 @@ public ServerComponentMarker SerializeInvocation(ServerComponentInvocationSequen
4543
var protectedBytes = _dataProtector.Protect(serializedServerComponentBytes, ServerComponentSerializationSettings.DataExpiration);
4644
return (serverComponent.Sequence, Convert.ToBase64String(protectedBytes));
4745
}
48-
49-
/// <remarks>
50-
/// Remember to update <see cref="PreambleBufferSize"/> if the number of entries being appended in this function changes.
51-
/// </remarks>
52-
internal static void AppendPreamble(TextWriter writer, ServerComponentMarker record)
53-
{
54-
var serializedStartRecord = JsonSerializer.Serialize(
55-
record,
56-
ServerComponentSerializationSettings.JsonSerializationOptions);
57-
58-
writer.Write("<!--Blazor:");
59-
writer.Write(serializedStartRecord);
60-
writer.Write("-->");
61-
}
62-
63-
internal static void AppendEpilogue(TextWriter writer, ServerComponentMarker record)
64-
{
65-
var endRecord = JsonSerializer.Serialize(
66-
record.GetEndRecord(),
67-
ServerComponentSerializationSettings.JsonSerializationOptions);
68-
69-
writer.Write("<!--Blazor:");
70-
writer.Write(endRecord);
71-
writer.Write("-->");
72-
}
7346
}

src/Components/Endpoints/src/DependencyInjection/WebAssemblyComponentSerializer.cs

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components.Endpoints;
88
// See the details of the component serialization protocol in WebAssemblyComponentDeserializer.cs on the Components solution.
99
internal sealed class WebAssemblyComponentSerializer
1010
{
11-
public static WebAssemblyComponentMarker SerializeInvocation(Type type, ParameterView parameters, string? key, bool prerendered)
11+
public static void SerializeInvocation(ref ComponentMarker marker, Type type, ParameterView parameters)
1212
{
1313
var assembly = type.Assembly.GetName().Name ?? throw new InvalidOperationException("Cannot prerender components from assemblies with a null name");
1414
var typeFullName = type.FullName ?? throw new InvalidOperationException("Cannot prerender component types with a null name");
@@ -19,29 +19,6 @@ public static WebAssemblyComponentMarker SerializeInvocation(Type type, Paramete
1919
var serializedDefinitions = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(definitions, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
2020
var serializedValues = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(values, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
2121

22-
return prerendered ? WebAssemblyComponentMarker.Prerendered(assembly, typeFullName, serializedDefinitions, serializedValues, key) :
23-
WebAssemblyComponentMarker.NonPrerendered(assembly, typeFullName, serializedDefinitions, serializedValues, key);
24-
}
25-
26-
internal static void AppendPreamble(TextWriter writer, WebAssemblyComponentMarker record)
27-
{
28-
var serializedStartRecord = JsonSerializer.Serialize(
29-
record,
30-
WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
31-
32-
writer.Write("<!--Blazor:");
33-
writer.Write(serializedStartRecord);
34-
writer.Write("-->");
35-
}
36-
37-
internal static void AppendEpilogue(TextWriter writer, WebAssemblyComponentMarker record)
38-
{
39-
var endRecord = JsonSerializer.Serialize(
40-
record.GetEndRecord(),
41-
WebAssemblyComponentSerializationSettings.JsonSerializationOptions);
42-
43-
writer.Write("<!--Blazor:");
44-
writer.Write(endRecord);
45-
writer.Write("-->");
22+
marker.WriteWebAssemblyData(assembly, typeFullName, serializedDefinitions, serializedValues);
4623
}
4724
}

src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,9 @@
2525
</ItemGroup>
2626

2727
<ItemGroup>
28+
<Compile Include="$(SharedSourceRoot)Components\ComponentMarker.cs" LinkBase="DependencyInjection" />
2829
<Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentSerializationSettings.cs" LinkBase="DependencyInjection" />
2930
<Compile Include="$(SharedSourceRoot)Components\ServerComponentSerializationSettings.cs" LinkBase="DependencyInjection" />
30-
<Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentMarker.cs" LinkBase="DependencyInjection" />
31-
<Compile Include="$(SharedSourceRoot)Components\ServerComponentMarker.cs" LinkBase="DependencyInjection" />
3231
<Compile Include="$(SharedSourceRoot)Components\ServerComponent.cs" LinkBase="DependencyInjection" />
3332
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" LinkBase="DependencyInjection" />
3433
<Compile Include="$(RepoRoot)src\Shared\Components\PrerenderComponentApplicationStore.cs" LinkBase="DependencyInjection" />

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

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Runtime.InteropServices;
55
using System.Text.Encodings.Web;
6+
using System.Text.Json;
67
using Microsoft.AspNetCore.Components.RenderTree;
78
using Microsoft.AspNetCore.Hosting;
89
using Microsoft.AspNetCore.Http;
@@ -199,29 +200,22 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo
199200
var componentState = (EndpointComponentState)GetComponentState(componentId);
200201
var renderBoundaryMarkers = allowBoundaryMarkers && componentState.StreamRendering;
201202

202-
// TODO: It's not clear that we actually want to emit the interactive component markers using this
203-
// HTML-comment syntax that we've used historically, plus we likely want some way to coalesce both
204-
// marker types into a single thing for auto mode (the code below emits both separately for auto).
205-
// It may be better to use a custom element like <blazor-component ...>[prerendered]<blazor-component>
206-
// so it's easier for the JS code to react automatically whenever this gets inserted or updated during
207-
// streaming SSR or progressively-enhanced navigation.
208-
var (serverMarker, webAssemblyMarker) = componentState.Component is SSRRenderModeBoundary boundary
209-
? boundary.ToMarkers(_httpContext, sequenceAndKey.Sequence, sequenceAndKey.Key)
210-
: default;
211-
212-
if (serverMarker.HasValue)
203+
ComponentEndMarker? endMarkerOrNull = default;
204+
205+
if (componentState.Component is SSRRenderModeBoundary boundary)
213206
{
214-
if (!_httpContext.Response.HasStarted)
207+
var marker = boundary.ToMarker(_httpContext, sequenceAndKey.Sequence, sequenceAndKey.Key);
208+
endMarkerOrNull = marker.ToEndMarker();
209+
210+
if (!_httpContext.Response.HasStarted && marker.Type is ComponentMarker.ServerMarkerType or ComponentMarker.AutoMarkerType)
215211
{
216212
_httpContext.Response.Headers.CacheControl = "no-cache, no-store, max-age=0";
217213
}
218214

219-
ServerComponentSerializer.AppendPreamble(output, serverMarker.Value);
220-
}
221-
222-
if (webAssemblyMarker.HasValue)
223-
{
224-
WebAssemblyComponentSerializer.AppendPreamble(output, webAssemblyMarker.Value);
215+
var serializedStartRecord = JsonSerializer.Serialize(marker, ServerComponentSerializationSettings.JsonSerializationOptions);
216+
output.Write("<!--Blazor:");
217+
output.Write(serializedStartRecord);
218+
output.Write("-->");
225219
}
226220

227221
if (renderBoundaryMarkers)
@@ -240,14 +234,12 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo
240234
output.Write("-->");
241235
}
242236

243-
if (webAssemblyMarker.HasValue && webAssemblyMarker.Value.PrerenderId is not null)
237+
if (endMarkerOrNull is { } endMarker)
244238
{
245-
WebAssemblyComponentSerializer.AppendEpilogue(output, webAssemblyMarker.Value);
246-
}
247-
248-
if (serverMarker.HasValue && serverMarker.Value.PrerenderId is not null)
249-
{
250-
ServerComponentSerializer.AppendEpilogue(output, serverMarker.Value);
239+
var serializedEndRecord = JsonSerializer.Serialize(endMarker, ServerComponentSerializationSettings.JsonSerializationOptions);
240+
output.Write("<!--Blazor:");
241+
output.Write(serializedEndRecord);
242+
output.Write("-->");
251243
}
252244
}
253245

src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Concurrent;
5+
using System.Diagnostics;
56
using System.Globalization;
67
using System.Security.Cryptography;
78
using System.Text;
@@ -99,7 +100,7 @@ private void Prerender(RenderTreeBuilder builder)
99100
builder.CloseComponent();
100101
}
101102

102-
public (ServerComponentMarker?, WebAssemblyComponentMarker?) ToMarkers(HttpContext httpContext, int sequence, object? key)
103+
public ComponentMarker ToMarker(HttpContext httpContext, int sequence, object? key)
103104
{
104105
// We expect that the '@key' and sequence number shouldn't change for a given component instance,
105106
// so we lazily compute the marker key once.
@@ -109,24 +110,30 @@ private void Prerender(RenderTreeBuilder builder)
109110
? ParameterView.Empty
110111
: ParameterView.FromDictionary((IDictionary<string, object?>)_latestParameters);
111112

112-
ServerComponentMarker? serverMarker = null;
113+
var marker = _renderMode switch
114+
{
115+
ServerRenderMode server => ComponentMarker.Create(ComponentMarker.ServerMarkerType, server.Prerender, _markerKey),
116+
WebAssemblyRenderMode webAssembly => ComponentMarker.Create(ComponentMarker.WebAssemblyMarkerType, webAssembly.Prerender, _markerKey),
117+
AutoRenderMode auto => ComponentMarker.Create(ComponentMarker.AutoMarkerType, auto.Prerender, _markerKey),
118+
_ => throw new UnreachableException($"Unknown render mode {_renderMode.GetType().FullName}"),
119+
};
120+
113121
if (_renderMode is ServerRenderMode or AutoRenderMode)
114122
{
115123
// Lazy because we don't actually want to require a whole chain of services including Data Protection
116124
// to be required unless you actually use Server render mode.
117125
var serverComponentSerializer = httpContext.RequestServices.GetRequiredService<ServerComponentSerializer>();
118126

119127
var invocationId = EndpointHtmlRenderer.GetOrCreateInvocationId(httpContext);
120-
serverMarker = serverComponentSerializer.SerializeInvocation(invocationId, _componentType, parameters, _markerKey, _prerender);
128+
serverComponentSerializer.SerializeInvocation(ref marker, invocationId, _componentType, parameters);
121129
}
122130

123-
WebAssemblyComponentMarker? webAssemblyMarker = null;
124131
if (_renderMode is WebAssemblyRenderMode or AutoRenderMode)
125132
{
126-
webAssemblyMarker = WebAssemblyComponentSerializer.SerializeInvocation(_componentType, parameters, _markerKey, _prerender);
133+
WebAssemblyComponentSerializer.SerializeInvocation(ref marker, _componentType, parameters);
127134
}
128135

129-
return (serverMarker, webAssemblyMarker);
136+
return marker;
130137
}
131138

132139
private string GenerateMarkerKey(int sequence, object? key)

0 commit comments

Comments
 (0)