File: Rendering\EndpointHtmlRenderer.PrerenderingState.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;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Components.Infrastructure;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
 
namespace Microsoft.AspNetCore.Components.Endpoints;
 
internal partial class EndpointHtmlRenderer
{
    private static readonly object InvokedRenderModesKey = new object();
 
    public async ValueTask<IHtmlContent> PrerenderPersistedStateAsync(HttpContext httpContext)
    {
        SetHttpContext(httpContext);
 
        var manager = _httpContext.RequestServices.GetRequiredService<ComponentStatePersistenceManager>();
 
        var renderModesMetadata = httpContext.GetEndpoint()?.Metadata.GetMetadata<ConfiguredRenderModesMetadata>();
 
        IPersistentComponentStateStore? store = null;
 
        // There is configured render modes metadata, use this to determine where to persist state if possible
        if (renderModesMetadata != null)
        {
            // No render modes are configured, do not persist state
            if (renderModesMetadata.ConfiguredRenderModes.Length == 0)
            {
                return ComponentStateHtmlContent.Empty;
            }
 
            // Single render mode, no need to perform inference. Any component that tried to render an
            // incompatible render mode would have failed at this point.
            if (renderModesMetadata.ConfiguredRenderModes.Length == 1)
            {
                store = renderModesMetadata.ConfiguredRenderModes[0] switch
                {
                    InteractiveServerRenderMode => new ProtectedPrerenderComponentApplicationStore(_httpContext.RequestServices.GetRequiredService<IDataProtectionProvider>()),
                    InteractiveWebAssemblyRenderMode => new PrerenderComponentApplicationStore(),
                    _ => throw new InvalidOperationException("Invalid configured render mode."),
                };
            }
        }
 
        if (store != null)
        {
            await manager.PersistStateAsync(store, this);
            return store switch
            {
                ProtectedPrerenderComponentApplicationStore protectedStore => new ComponentStateHtmlContent(protectedStore, null),
                PrerenderComponentApplicationStore prerenderStore => new ComponentStateHtmlContent(null, prerenderStore),
                _ => throw new InvalidOperationException("Invalid store."),
            };
        }
        else
        {
            // We were not able to resolve a store from the configured render modes metadata, we need to capture
            // all possible destinations for the state and persist it in all of them.
            var serverStore = new ProtectedPrerenderComponentApplicationStore(_httpContext.RequestServices.GetRequiredService<IDataProtectionProvider>());
            var webAssemblyStore = new PrerenderComponentApplicationStore();
 
            // The persistence state manager checks if the store implements
            // IEnumerable<IPersistentComponentStateStore> and if so, it invokes PersistStateAsync on each store
            // for each of the render mode callbacks defined.
            // We pass in a composite store with fake stores for each render mode that only take care of
            // creating a copy of the state for each render mode.
            // Then, we copy the state from the auto store to the server and webassembly stores and persist
            // the real state for server and webassembly render modes.
            // This makes sure that:
            // 1. The persistence state manager is agnostic to the render modes.
            // 2. The callbacks are run only once, even if the state ends up persisted in multiple locations.
            var server = new CopyOnlyStore<InteractiveServerRenderMode>();
            var auto = new CopyOnlyStore<InteractiveAutoRenderMode>();
            var webAssembly = new CopyOnlyStore<InteractiveWebAssemblyRenderMode>();
            store = new CompositeStore(server, auto, webAssembly);
 
            await manager.PersistStateAsync(store, this);
 
            foreach (var kvp in auto.Saved)
            {
                server.Saved.Add(kvp.Key, kvp.Value);
                webAssembly.Saved.Add(kvp.Key, kvp.Value);
            }
 
            // Persist state only if there is state to persist
            var saveServerTask = server.Saved.Count > 0
                ? serverStore.PersistStateAsync(server.Saved)
                : Task.CompletedTask;
 
            var saveWebAssemblyTask = webAssembly.Saved.Count > 0
                ? webAssemblyStore.PersistStateAsync(webAssembly.Saved)
                : Task.CompletedTask;
 
            await Task.WhenAll(
                saveServerTask,
                saveWebAssemblyTask);
 
            // Do not return any HTML content if there is no state to persist for a given mode.
            return new ComponentStateHtmlContent(
                server.Saved.Count > 0 ? serverStore : null,
                webAssembly.Saved.Count > 0 ? webAssemblyStore : null);
        }
    }
 
    public async ValueTask<IHtmlContent> PrerenderPersistedStateAsync(HttpContext httpContext, PersistedStateSerializationMode serializationMode)
    {
        SetHttpContext(httpContext);
 
        // First we resolve "infer" mode to a specific mode
        if (serializationMode == PersistedStateSerializationMode.Infer)
        {
            switch (GetPersistStateRenderMode(_httpContext))
            {
                case InvokedRenderModes.Mode.None:
                    return ComponentStateHtmlContent.Empty;
                case InvokedRenderModes.Mode.ServerAndWebAssembly:
                    throw new InvalidOperationException(
                        Resources.FailedToInferComponentPersistenceMode);
                case InvokedRenderModes.Mode.Server:
                    serializationMode = PersistedStateSerializationMode.Server;
                    break;
                case InvokedRenderModes.Mode.WebAssembly:
                    serializationMode = PersistedStateSerializationMode.WebAssembly;
                    break;
                default:
                    throw new InvalidOperationException("Invalid persistence mode");
            }
        }
 
        var manager = _httpContext.RequestServices.GetRequiredService<ComponentStatePersistenceManager>();
 
        // Now given the mode, we obtain a particular store for that mode
        // and persist the state and return the HTML content
        switch (serializationMode)
        {
            case PersistedStateSerializationMode.Server:
                var protectedStore = new ProtectedPrerenderComponentApplicationStore(_httpContext.RequestServices.GetRequiredService<IDataProtectionProvider>());
                await manager.PersistStateAsync(protectedStore, this);
                return new ComponentStateHtmlContent(protectedStore, null);
 
            case PersistedStateSerializationMode.WebAssembly:
                var store = new PrerenderComponentApplicationStore();
                await manager.PersistStateAsync(store, this);
                return new ComponentStateHtmlContent(null, store);
 
            default:
                throw new InvalidOperationException("Invalid persistence mode.");
        }
    }
 
    // Internal for test only
    internal static void UpdateSaveStateRenderMode(HttpContext httpContext, IComponentRenderMode? mode)
    {
        // TODO: This will all have to change when we support multiple render modes in the same response
        if (ModeEnablesPrerendering(mode))
        {
            var currentInvocation = mode switch
            {
                InteractiveServerRenderMode => InvokedRenderModes.Mode.Server,
                InteractiveWebAssemblyRenderMode => InvokedRenderModes.Mode.WebAssembly,
                InteractiveAutoRenderMode => throw new NotImplementedException("TODO: To be able to support InteractiveAutoRenderMode, we have to serialize persisted state in both WebAssembly and Server formats, or unify the two formats."),
                _ => throw new ArgumentException(Resources.FormatUnsupportedRenderMode(mode), nameof(mode)),
            };
 
            if (!httpContext.Items.TryGetValue(InvokedRenderModesKey, out var result))
            {
                httpContext.Items[InvokedRenderModesKey] = new InvokedRenderModes(currentInvocation);
            }
            else
            {
                var invokedMode = (InvokedRenderModes)result!;
                if (invokedMode.Value != currentInvocation)
                {
                    invokedMode.Value = InvokedRenderModes.Mode.ServerAndWebAssembly;
                }
            }
        }
    }
 
    private static bool ModeEnablesPrerendering(IComponentRenderMode? mode) => mode switch
    {
        InteractiveServerRenderMode { Prerender: true } => true,
        InteractiveWebAssemblyRenderMode { Prerender: true } => true,
        InteractiveAutoRenderMode { Prerender: true } => true,
        _ => false
    };
 
    internal static InvokedRenderModes.Mode GetPersistStateRenderMode(HttpContext httpContext)
    {
        return httpContext.Items.TryGetValue(InvokedRenderModesKey, out var result)
            ? ((InvokedRenderModes)result!).Value
            : InvokedRenderModes.Mode.None;
    }
 
    internal sealed class ComponentStateHtmlContent : IHtmlContent
    {
        public static ComponentStateHtmlContent Empty { get; } = new(null, null);
 
        internal PrerenderComponentApplicationStore? ServerStore { get; }
 
        internal PrerenderComponentApplicationStore? WebAssemblyStore { get; }
 
        public ComponentStateHtmlContent(PrerenderComponentApplicationStore? serverStore, PrerenderComponentApplicationStore? webAssemblyStore)
        {
            WebAssemblyStore = webAssemblyStore;
            ServerStore = serverStore;
        }
 
        public void WriteTo(TextWriter writer, HtmlEncoder encoder)
        {
            if (ServerStore is not null && ServerStore.PersistedState is not null)
            {
                writer.Write("<!--Blazor-Server-Component-State:");
                writer.Write(ServerStore.PersistedState);
                writer.Write("-->");
            }
 
            if (WebAssemblyStore is not null && WebAssemblyStore.PersistedState is not null)
            {
                writer.Write("<!--Blazor-WebAssembly-Component-State:");
                writer.Write(WebAssemblyStore.PersistedState);
                writer.Write("-->");
            }
        }
    }
 
    internal class CompositeStore : IPersistentComponentStateStore, IEnumerable<IPersistentComponentStateStore>
    {
        public CompositeStore(
            CopyOnlyStore<InteractiveServerRenderMode> server,
            CopyOnlyStore<InteractiveAutoRenderMode> auto,
            CopyOnlyStore<InteractiveWebAssemblyRenderMode> webassembly)
        {
            Server = server;
            Auto = auto;
            Webassembly = webassembly;
        }
 
        public CopyOnlyStore<InteractiveServerRenderMode> Server { get; }
        public CopyOnlyStore<InteractiveAutoRenderMode> Auto { get; }
        public CopyOnlyStore<InteractiveWebAssemblyRenderMode> Webassembly { get; }
 
        public IEnumerator<IPersistentComponentStateStore> GetEnumerator()
        {
            yield return Server;
            yield return Auto;
            yield return Webassembly;
        }
 
        public Task<IDictionary<string, byte[]>> GetPersistedStateAsync() => throw new NotImplementedException();
 
        public Task PersistStateAsync(IReadOnlyDictionary<string, byte[]> state) => Task.CompletedTask;
 
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
 
    internal class CopyOnlyStore<T> : IPersistentComponentStateStore where T : IComponentRenderMode
    {
        public Dictionary<string, byte[]> Saved { get; private set; } = new();
 
        public Task<IDictionary<string, byte[]>> GetPersistedStateAsync() => throw new NotImplementedException();
 
        public Task PersistStateAsync(IReadOnlyDictionary<string, byte[]> state)
        {
            Saved = new Dictionary<string, byte[]>(state);
            return Task.CompletedTask;
        }
 
        public bool SupportsRenderMode(IComponentRenderMode renderMode) => renderMode is T;
    }
}