File: Rendering\SSRRenderModeBoundary.cs
Web Access
Project: src\src\Components\Endpoints\src\Microsoft.AspNetCore.Components.Endpoints.csproj (Microsoft.AspNetCore.Components.Endpoints)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using static Microsoft.AspNetCore.Internal.LinkerFlags;
 
namespace Microsoft.AspNetCore.Components.Endpoints;
 
/// <summary>
/// A component that describes a location in prerendered output where client-side code
/// should insert an interactive component.
/// </summary>
internal class SSRRenderModeBoundary : IComponent
{
    private static readonly ConcurrentDictionary<Type, string> _componentTypeNameHashCache = new();
 
    [DynamicallyAccessedMembers(Component)]
    private readonly Type _componentType;
    private readonly bool _prerender;
    private RenderHandle _renderHandle;
    private IReadOnlyDictionary<string, object?>? _latestParameters;
    private ComponentMarkerKey? _markerKey;
 
    public IComponentRenderMode RenderMode { get; }
 
    public SSRRenderModeBoundary(
        HttpContext httpContext,
        [DynamicallyAccessedMembers(Component)] Type componentType,
        IComponentRenderMode renderMode)
    {
        AssertRenderModeIsConfigured(httpContext, componentType, renderMode);
 
        _componentType = componentType;
        RenderMode = renderMode;
        _prerender = renderMode switch
        {
            InteractiveServerRenderMode mode => mode.Prerender,
            InteractiveWebAssemblyRenderMode mode => mode.Prerender,
            InteractiveAutoRenderMode mode => mode.Prerender,
            _ => throw new ArgumentException($"Server-side rendering does not support the render mode '{renderMode}'.", nameof(renderMode))
        };
    }
 
    private static void AssertRenderModeIsConfigured(HttpContext httpContext, Type componentType, IComponentRenderMode renderMode)
    {
        var configuredRenderModesMetadata = httpContext.GetEndpoint()?.Metadata.GetMetadata<ConfiguredRenderModesMetadata>();
        if (configuredRenderModesMetadata is null)
        {
            // This is not a Razor Components endpoint. It might be that the app is using RazorComponentResult,
            // or perhaps something else has changed the endpoint dynamically. In this case we don't know how
            // the app is configured so we just proceed and allow any errors to happen if the client-side code
            // later tries to reach endpoints that aren't mapped.
            return;
        }
 
        var configuredModes = configuredRenderModesMetadata.ConfiguredRenderModes;
 
        // We have to allow for specified rendermodes being subclases of the known types
        if (renderMode is InteractiveServerRenderMode || renderMode is InteractiveAutoRenderMode)
        {
            AssertRenderModeIsConfigured<InteractiveServerRenderMode>(componentType, renderMode, configuredModes, "AddInteractiveServerRenderMode");
        }
 
        if (renderMode is InteractiveWebAssemblyRenderMode || renderMode is InteractiveAutoRenderMode)
        {
            AssertRenderModeIsConfigured<InteractiveWebAssemblyRenderMode>(componentType, renderMode, configuredModes, "AddInteractiveWebAssemblyRenderMode");
        }
    }
 
    private static void AssertRenderModeIsConfigured<TRequiredMode>(Type componentType, IComponentRenderMode specifiedMode, IComponentRenderMode[] configuredModes, string expectedCall) where TRequiredMode : IComponentRenderMode
    {
        foreach (var configuredMode in configuredModes)
        {
            // We have to allow for configured rendermodes being subclases of the known types
            if (configuredMode is TRequiredMode)
            {
                return;
            }
        }
 
        throw new InvalidOperationException($"A component of type '{componentType}' has render mode '{specifiedMode.GetType().Name}', " +
            $"but the required endpoints are not mapped on the server. When calling " +
            $"'{nameof(RazorComponentsEndpointRouteBuilderExtensions.MapRazorComponents)}', add a call to " +
            $"'{expectedCall}'. For example, " +
            $"'builder.{nameof(RazorComponentsEndpointRouteBuilderExtensions.MapRazorComponents)}<...>.{expectedCall}()'");
    }
 
    public void Attach(RenderHandle renderHandle)
    {
        _renderHandle = renderHandle;
    }
 
    public Task SetParametersAsync(ParameterView parameters)
    {
        // We have to snapshot the parameters because ParameterView is like a ref struct - it can't escape the
        // call stack because the underlying buffer may get reused. This is enforced through a runtime check.
        _latestParameters = parameters.ToDictionary();
 
        ValidateParameters(_latestParameters);
 
        if (_prerender)
        {
            _renderHandle.Render(Prerender);
        }
 
        return Task.CompletedTask;
    }
 
    private void ValidateParameters(IReadOnlyDictionary<string, object?> latestParameters)
    {
        foreach (var (name, value) in latestParameters)
        {
            // There are many other things we can't serialize too, but give special errors for Delegate because
            // it may be a common mistake to try passing ChildContent when crossing rendermode boundaries.
            if (value is Delegate)
            {
                var valueType = value.GetType();
                if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(RenderFragment<>))
                {
                    throw new InvalidOperationException($"Cannot pass RenderFragment<T> parameter '{name}' to component '{_componentType.Name}' with rendermode '{RenderMode.GetType().Name}'. Templated content can't be passed across a rendermode boundary, because it is arbitrary code and cannot be serialized.");
                }
                else
                {
                    // TODO: Ideally we *should* support RenderFragment (the non-generic version) by prerendering it
                    // However it's very nontrivial since it means we have to execute it within the current renderer
                    // somehow without actually emitting its result directly, wait for quiescence, and then prerender
                    // the output into a separate buffer so we can serialize it in a special way.
                    // A prototype implementation is at https://github.com/dotnet/aspnetcore/commit/ed330ff5b143974d9060828a760ad486b1d386ac
                    throw new InvalidOperationException($"Cannot pass the parameter '{name}' to component '{_componentType.Name}' with rendermode '{RenderMode.GetType().Name}'. This is because the parameter is of the delegate type '{value.GetType()}', which is arbitrary code and cannot be serialized.");
                }
            }
        }
    }
 
    private void Prerender(RenderTreeBuilder builder)
    {
        builder.OpenComponent(0, _componentType);
 
        foreach (var (name, value) in _latestParameters!)
        {
            builder.AddComponentParameter(1, name, value);
        }
 
        builder.CloseComponent();
    }
 
    public ComponentMarker ToMarker(HttpContext httpContext, int sequence, object? componentKey)
    {
        // We expect that the '@key' and sequence number shouldn't change for a given component instance,
        // so we lazily compute the marker key once.
        _markerKey ??= GenerateMarkerKey(sequence, componentKey);
 
        var parameters = _latestParameters is null
            ? ParameterView.Empty
            : ParameterView.FromDictionary((IDictionary<string, object?>)_latestParameters);
 
        var marker = RenderMode switch
        {
            InteractiveServerRenderMode server => ComponentMarker.Create(ComponentMarker.ServerMarkerType, server.Prerender, _markerKey),
            InteractiveWebAssemblyRenderMode webAssembly => ComponentMarker.Create(ComponentMarker.WebAssemblyMarkerType, webAssembly.Prerender, _markerKey),
            InteractiveAutoRenderMode auto => ComponentMarker.Create(ComponentMarker.AutoMarkerType, auto.Prerender, _markerKey),
            _ => throw new UnreachableException($"Unknown render mode {RenderMode.GetType().FullName}"),
        };
 
        if (RenderMode is InteractiveServerRenderMode or InteractiveAutoRenderMode)
        {
            // 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);
            serverComponentSerializer.SerializeInvocation(ref marker, invocationId, _componentType, parameters);
        }
 
        if (RenderMode is InteractiveWebAssemblyRenderMode or InteractiveAutoRenderMode)
        {
            WebAssemblyComponentSerializer.SerializeInvocation(ref marker, _componentType, parameters);
        }
 
        return marker;
    }
 
    private ComponentMarkerKey GenerateMarkerKey(int sequence, object? componentKey)
    {
        var componentTypeNameHash = _componentTypeNameHashCache.GetOrAdd(_componentType, TypeNameHash.Compute);
        var sequenceString = sequence.ToString(CultureInfo.InvariantCulture);
 
        var locationHash = $"{componentTypeNameHash}:{sequenceString}";
        var formattedComponentKey = (componentKey as IFormattable)?.ToString(null, CultureInfo.InvariantCulture) ?? string.Empty;
 
        return new()
        {
            LocationHash = locationHash,
            FormattedComponentKey = formattedComponentKey,
        };
    }
}