| File: Rendering\EndpointHtmlRenderer.PrerenderingState.cs | Web Access |
| Project: src\aspnetcore\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; } }