Skip to content

[Blazor] Support for determining the render mode dynamically #49477

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 6 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,17 @@ namespace Microsoft.AspNetCore.Components.Endpoints;
// See the details of the component serialization protocol in ServerComponentDeserializer.cs on the Components solution.
internal sealed class ServerComponentSerializer
{
public const int PreambleBufferSize = 3;

private readonly ITimeLimitedDataProtector _dataProtector;

public ServerComponentSerializer(IDataProtectionProvider dataProtectionProvider) =>
_dataProtector = dataProtectionProvider
.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();

public ServerComponentMarker SerializeInvocation(ServerComponentInvocationSequence invocationId, Type type, ParameterView parameters, string key, bool prerendered)
public void SerializeInvocation(ref ComponentMarker marker, ServerComponentInvocationSequence invocationId, Type type, ParameterView parameters)
{
var (sequence, serverComponent) = CreateSerializedServerComponent(invocationId, type, parameters);
return prerendered ? ServerComponentMarker.Prerendered(sequence, serverComponent, key) : ServerComponentMarker.NonPrerendered(sequence, serverComponent, key);
marker.WriteServerData(sequence, serverComponent);
}

private (int sequence, string payload) CreateSerializedServerComponent(
Expand All @@ -45,29 +43,4 @@ public ServerComponentMarker SerializeInvocation(ServerComponentInvocationSequen
var protectedBytes = _dataProtector.Protect(serializedServerComponentBytes, ServerComponentSerializationSettings.DataExpiration);
return (serverComponent.Sequence, Convert.ToBase64String(protectedBytes));
}

/// <remarks>
/// Remember to update <see cref="PreambleBufferSize"/> if the number of entries being appended in this function changes.
/// </remarks>
internal static void AppendPreamble(TextWriter writer, ServerComponentMarker record)
{
var serializedStartRecord = JsonSerializer.Serialize(
record,
ServerComponentSerializationSettings.JsonSerializationOptions);

writer.Write("<!--Blazor:");
writer.Write(serializedStartRecord);
writer.Write("-->");
}

internal static void AppendEpilogue(TextWriter writer, ServerComponentMarker record)
{
var endRecord = JsonSerializer.Serialize(
record.GetEndRecord(),
ServerComponentSerializationSettings.JsonSerializationOptions);

writer.Write("<!--Blazor:");
writer.Write(endRecord);
writer.Write("-->");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components.Endpoints;
// See the details of the component serialization protocol in WebAssemblyComponentDeserializer.cs on the Components solution.
internal sealed class WebAssemblyComponentSerializer
{
public static WebAssemblyComponentMarker SerializeInvocation(Type type, ParameterView parameters, string? key, bool prerendered)
public static void SerializeInvocation(ref ComponentMarker marker, Type type, ParameterView parameters)
{
var assembly = type.Assembly.GetName().Name ?? throw new InvalidOperationException("Cannot prerender components from assemblies with a null name");
var typeFullName = type.FullName ?? throw new InvalidOperationException("Cannot prerender component types with a null name");
Expand All @@ -19,29 +19,6 @@ public static WebAssemblyComponentMarker SerializeInvocation(Type type, Paramete
var serializedDefinitions = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(definitions, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));
var serializedValues = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(values, WebAssemblyComponentSerializationSettings.JsonSerializationOptions));

return prerendered ? WebAssemblyComponentMarker.Prerendered(assembly, typeFullName, serializedDefinitions, serializedValues, key) :
WebAssemblyComponentMarker.NonPrerendered(assembly, typeFullName, serializedDefinitions, serializedValues, key);
}

internal static void AppendPreamble(TextWriter writer, WebAssemblyComponentMarker record)
{
var serializedStartRecord = JsonSerializer.Serialize(
record,
WebAssemblyComponentSerializationSettings.JsonSerializationOptions);

writer.Write("<!--Blazor:");
writer.Write(serializedStartRecord);
writer.Write("-->");
}

internal static void AppendEpilogue(TextWriter writer, WebAssemblyComponentMarker record)
{
var endRecord = JsonSerializer.Serialize(
record.GetEndRecord(),
WebAssemblyComponentSerializationSettings.JsonSerializationOptions);

writer.Write("<!--Blazor:");
writer.Write(endRecord);
writer.Write("-->");
marker.WriteWebAssemblyData(assembly, typeFullName, serializedDefinitions, serializedValues);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@
</ItemGroup>

<ItemGroup>
<Compile Include="$(SharedSourceRoot)Components\ComponentMarker.cs" LinkBase="DependencyInjection" />
<Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentSerializationSettings.cs" LinkBase="DependencyInjection" />
<Compile Include="$(SharedSourceRoot)Components\ServerComponentSerializationSettings.cs" LinkBase="DependencyInjection" />
<Compile Include="$(SharedSourceRoot)Components\WebAssemblyComponentMarker.cs" LinkBase="DependencyInjection" />
<Compile Include="$(SharedSourceRoot)Components\ServerComponentMarker.cs" LinkBase="DependencyInjection" />
<Compile Include="$(SharedSourceRoot)Components\ServerComponent.cs" LinkBase="DependencyInjection" />
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" LinkBase="DependencyInjection" />
<Compile Include="$(RepoRoot)src\Shared\Components\PrerenderComponentApplicationStore.cs" LinkBase="DependencyInjection" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Runtime.InteropServices;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -199,29 +200,22 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo
var componentState = (EndpointComponentState)GetComponentState(componentId);
var renderBoundaryMarkers = allowBoundaryMarkers && componentState.StreamRendering;

// TODO: It's not clear that we actually want to emit the interactive component markers using this
// HTML-comment syntax that we've used historically, plus we likely want some way to coalesce both
// marker types into a single thing for auto mode (the code below emits both separately for auto).
// It may be better to use a custom element like <blazor-component ...>[prerendered]<blazor-component>
// so it's easier for the JS code to react automatically whenever this gets inserted or updated during
// streaming SSR or progressively-enhanced navigation.
var (serverMarker, webAssemblyMarker) = componentState.Component is SSRRenderModeBoundary boundary
? boundary.ToMarkers(_httpContext, sequenceAndKey.Sequence, sequenceAndKey.Key)
: default;

if (serverMarker.HasValue)
ComponentEndMarker? endMarkerOrNull = default;

if (componentState.Component is SSRRenderModeBoundary boundary)
{
if (!_httpContext.Response.HasStarted)
var marker = boundary.ToMarker(_httpContext, sequenceAndKey.Sequence, sequenceAndKey.Key);
endMarkerOrNull = marker.ToEndMarker();

if (!_httpContext.Response.HasStarted && marker.Type is ComponentMarker.ServerMarkerType or ComponentMarker.AutoMarkerType)
{
_httpContext.Response.Headers.CacheControl = "no-cache, no-store, max-age=0";
}

ServerComponentSerializer.AppendPreamble(output, serverMarker.Value);
}

if (webAssemblyMarker.HasValue)
{
WebAssemblyComponentSerializer.AppendPreamble(output, webAssemblyMarker.Value);
var serializedStartRecord = JsonSerializer.Serialize(marker, ServerComponentSerializationSettings.JsonSerializationOptions);
output.Write("<!--Blazor:");
output.Write(serializedStartRecord);
output.Write("-->");
}

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

if (webAssemblyMarker.HasValue && webAssemblyMarker.Value.PrerenderId is not null)
if (endMarkerOrNull is { } endMarker)
{
WebAssemblyComponentSerializer.AppendEpilogue(output, webAssemblyMarker.Value);
}

if (serverMarker.HasValue && serverMarker.Value.PrerenderId is not null)
{
ServerComponentSerializer.AppendEpilogue(output, serverMarker.Value);
var serializedEndRecord = JsonSerializer.Serialize(endMarker, ServerComponentSerializationSettings.JsonSerializationOptions);
output.Write("<!--Blazor:");
output.Write(serializedEndRecord);
output.Write("-->");
}
}

Expand Down
19 changes: 13 additions & 6 deletions src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
Expand Down Expand Up @@ -99,7 +100,7 @@ private void Prerender(RenderTreeBuilder builder)
builder.CloseComponent();
}

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

ServerComponentMarker? serverMarker = null;
var marker = _renderMode switch
{
ServerRenderMode server => ComponentMarker.Create(ComponentMarker.ServerMarkerType, server.Prerender, _markerKey),
WebAssemblyRenderMode webAssembly => ComponentMarker.Create(ComponentMarker.WebAssemblyMarkerType, webAssembly.Prerender, _markerKey),
AutoRenderMode auto => ComponentMarker.Create(ComponentMarker.AutoMarkerType, auto.Prerender, _markerKey),
_ => throw new UnreachableException($"Unknown render mode {_renderMode.GetType().FullName}"),
};

if (_renderMode is ServerRenderMode or AutoRenderMode)
{
// Lazy because we don't actually want to require a whole chain of services including Data Protection
// to be required unless you actually use Server render mode.
var serverComponentSerializer = httpContext.RequestServices.GetRequiredService<ServerComponentSerializer>();

var invocationId = EndpointHtmlRenderer.GetOrCreateInvocationId(httpContext);
serverMarker = serverComponentSerializer.SerializeInvocation(invocationId, _componentType, parameters, _markerKey, _prerender);
serverComponentSerializer.SerializeInvocation(ref marker, invocationId, _componentType, parameters);
}

WebAssemblyComponentMarker? webAssemblyMarker = null;
if (_renderMode is WebAssemblyRenderMode or AutoRenderMode)
{
webAssemblyMarker = WebAssemblyComponentSerializer.SerializeInvocation(_componentType, parameters, _markerKey, _prerender);
WebAssemblyComponentSerializer.SerializeInvocation(ref marker, _componentType, parameters);
}

return (serverMarker, webAssemblyMarker);
return marker;
}

private string GenerateMarkerKey(int sequence, object? key)
Expand Down
Loading