Преглед изворни кода

[Blazor] Applies API review feedback and removes byte array APIs from the public API surface (#35768)

* Applies API review feedback
* Removes byte array based overloads
Javier Calvarro Nelson пре 4 година
родитељ
комит
b849efe77a
31 измењених фајлова са 496 додато и 281 уклоњено
  1. 2 5
      src/Components/Components/src/IPersistentComponentStateStore.cs
  2. 27 22
      src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs
  3. 60 89
      src/Components/Components/src/PersistentComponentState.cs
  4. 32 0
      src/Components/Components/src/PersistingComponentStateSubscription.cs
  5. 14 15
      src/Components/Components/src/PublicAPI.Unshipped.txt
  6. 69 26
      src/Components/Components/test/Lifetime/ComponentApplicationLifetimeTest.cs
  7. 35 27
      src/Components/Components/test/Lifetime/ComponentApplicationStateTest.cs
  8. 4 2
      src/Components/ComponentsNoDeps.slnf
  9. 3 3
      src/Components/Server/src/Circuits/CircuitFactory.cs
  10. 4 5
      src/Components/Server/src/Circuits/CircuitHandleRegistry.cs
  11. 2 2
      src/Components/Server/src/Circuits/ICircuitFactory.cs
  12. 4 4
      src/Components/Server/src/Circuits/ICircuitHandleRegistry.cs
  13. 1 1
      src/Components/Server/src/Circuits/IServerComponentDeserializer.cs
  14. 7 6
      src/Components/Server/test/Circuits/ComponentHubTest.cs
  15. 12 6
      src/Components/WebAssembly/Samples/HostedBlazorWebassemblyApp/Client/Pages/FetchData.razor
  16. 2 3
      src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs
  17. 4 3
      src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs
  18. 2 2
      src/Components/test/E2ETest/Tests/CircuitTests.cs
  19. 27 9
      src/Components/test/testassets/BasicTestApp/PreserveStateComponent.razor
  20. 6 8
      src/Components/test/testassets/BasicTestApp/PreserveStateService.cs
  21. 1 1
      src/Components/test/testassets/TestServer/Properties/launchSettings.json
  22. 24 3
      src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs
  23. 2 3
      src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs
  24. 8 4
      src/Mvc/Mvc.TagHelpers/test/PrerenderComponentApplicationStoreTest.cs
  25. 5 1
      src/Mvc/Mvc.TagHelpers/test/ProtectedPrerenderComponentApplicationStateTest.cs
  26. 3 3
      src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs
  27. 2 2
      src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs
  28. 4 4
      src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentRendererTest.cs
  29. 107 0
      src/Mvc/perf/Microbenchmarks/Microsoft.AspNetCore.Mvc/PreserveComponentStateBenchmark.cs
  30. 18 15
      src/Shared/Components/PrerenderComponentApplicationStore.cs
  31. 5 7
      src/Shared/Components/ProtectedPrerenderComponentApplicationStore.cs

+ 2 - 5
src/Components/Components/src/Lifetime/IComponentApplicationStateStore.cs → src/Components/Components/src/IPersistentComponentStateStore.cs

@@ -1,15 +1,12 @@
 // 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.Generic;
-using System.Threading.Tasks;
-
-namespace Microsoft.AspNetCore.Components.Lifetime
+namespace Microsoft.AspNetCore.Components
 {
     /// <summary>
     /// Manages the storage for components and services that are part of a Blazor application.
     /// </summary>
-    public interface IComponentApplicationStateStore
+    public interface IPersistentComponentStateStore
     {
         /// <summary>
         /// Gets the persisted state from the store.

+ 27 - 22
src/Components/Components/src/Lifetime/ComponentApplicationLifetime.cs → src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs

@@ -2,56 +2,60 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.Buffers;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
+using System.IO.Pipelines;
+using System.Text.Json;
+using System.Text.Json.Serialization;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components.RenderTree;
 using Microsoft.Extensions.Logging;
 
-namespace Microsoft.AspNetCore.Components.Lifetime
+namespace Microsoft.AspNetCore.Components.Infrastructure
 {
     /// <summary>
-    /// Manages the lifetime of a component application.
+    /// Manages the persistent state of components in an application.
     /// </summary>
-    public class ComponentApplicationLifetime
+    public class ComponentStatePersistenceManager
     {
         private bool _stateIsPersisted;
-        private readonly List<ComponentApplicationState.OnPersistingCallback> _pauseCallbacks = new();
-        private readonly Dictionary<string, byte[]> _currentState = new();
-        private readonly ILogger<ComponentApplicationLifetime> _logger;
+        private readonly List<Func<Task>> _pauseCallbacks = new();
+        private readonly Dictionary<string, byte[]> _currentState = new(StringComparer.Ordinal);
+        private readonly ILogger<ComponentStatePersistenceManager> _logger;
 
         /// <summary>
-        /// Initializes a new instance of <see cref="ComponentApplicationLifetime"/>.
+        /// Initializes a new instance of <see cref="ComponentStatePersistenceManager"/>.
         /// </summary>
-        public ComponentApplicationLifetime(ILogger<ComponentApplicationLifetime> logger)
+        public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager> logger)
         {
-            State = new ComponentApplicationState(_currentState, _pauseCallbacks);
+            State = new PersistentComponentState(_currentState, _pauseCallbacks);
             _logger = logger;
         }
 
         /// <summary>
-        /// Gets the <see cref="ComponentApplicationState"/> associated with the <see cref="ComponentApplicationLifetime"/>.
+        /// Gets the <see cref="ComponentStatePersistenceManager"/> associated with the <see cref="ComponentStatePersistenceManager"/>.
         /// </summary>
-        public ComponentApplicationState State { get; }
+        public PersistentComponentState State { get; }
 
         /// <summary>
-        /// Restores the component application state from the given <see cref="IComponentApplicationStateStore"/>.
+        /// Restores the component application state from the given <see cref="IPersistentComponentStateStore"/>.
         /// </summary>
-        /// <param name="store">The <see cref="IComponentApplicationStateStore"/> to restore the application state from.</param>
+        /// <param name="store">The <see cref="IPersistentComponentStateStore"/> to restore the application state from.</param>
         /// <returns>A <see cref="Task"/> that will complete when the state has been restored.</returns>
-        public async Task RestoreStateAsync(IComponentApplicationStateStore store)
+        public async Task RestoreStateAsync(IPersistentComponentStateStore store)
         {
             var data = await store.GetPersistedStateAsync();
             State.InitializeExistingState(data);
         }
 
         /// <summary>
-        /// Persists the component application state into the given <see cref="IComponentApplicationStateStore"/>.
+        /// Persists the component application state into the given <see cref="IPersistentComponentStateStore"/>.
         /// </summary>
-        /// <param name="store">The <see cref="IComponentApplicationStateStore"/> to restore the application state from.</param>
+        /// <param name="store">The <see cref="IPersistentComponentStateStore"/> to restore the application state from.</param>
         /// <param name="renderer">The <see cref="Renderer"/> that components are being rendered.</param>
         /// <returns>A <see cref="Task"/> that will complete when the state has been restored.</returns>
-        public Task PersistStateAsync(IComponentApplicationStateStore store, Renderer renderer)
+        public Task PersistStateAsync(IPersistentComponentStateStore store, Renderer renderer)
         {
             if (_stateIsPersisted)
             {
@@ -64,10 +68,11 @@ namespace Microsoft.AspNetCore.Components.Lifetime
 
             async Task PauseAndPersistState()
             {
+                State.PersistingState = true;
                 await PauseAsync();
+                State.PersistingState = false;
 
-                var data = new ReadOnlyDictionary<string, byte[]>(_currentState);
-                await store.PersistStateAsync(data);
+                await store.PersistStateAsync(_currentState);
             }
         }
 
@@ -75,7 +80,7 @@ namespace Microsoft.AspNetCore.Components.Lifetime
         {
             List<Task>? pendingCallbackTasks = null;
 
-            for (int i = 0; i < _pauseCallbacks.Count; i++)
+            for (var i = 0; i < _pauseCallbacks.Count; i++)
             {
                 var callback = _pauseCallbacks[i];
                 var result = ExecuteCallback(callback, _logger);
@@ -95,7 +100,7 @@ namespace Microsoft.AspNetCore.Components.Lifetime
                 return Task.CompletedTask;
             }
 
-            static Task ExecuteCallback(ComponentApplicationState.OnPersistingCallback callback, ILogger<ComponentApplicationLifetime> logger)
+            static Task ExecuteCallback(Func<Task> callback, ILogger<ComponentStatePersistenceManager> logger)
             {
                 try
                 {
@@ -115,7 +120,7 @@ namespace Microsoft.AspNetCore.Components.Lifetime
                     return Task.CompletedTask;
                 }
 
-                static async Task Awaited(Task task, ILogger<ComponentApplicationLifetime> logger)
+                static async Task Awaited(Task task, ILogger<ComponentStatePersistenceManager> logger)
                 {
                     try
                     {

+ 60 - 89
src/Components/Components/src/Lifetime/ComponentApplicationState.cs → src/Components/Components/src/PersistentComponentState.cs

@@ -1,11 +1,9 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System;
-using System.Collections.Generic;
+using System.Buffers;
 using System.Diagnostics.CodeAnalysis;
 using System.Text.Json;
-using System.Threading.Tasks;
 using static Microsoft.AspNetCore.Internal.LinkerFlags;
 
 namespace Microsoft.AspNetCore.Components
@@ -13,163 +11,136 @@ namespace Microsoft.AspNetCore.Components
     /// <summary>
     /// The state for the components and services of a components application.
     /// </summary>
-    public class ComponentApplicationState
+    public class PersistentComponentState
     {
         private IDictionary<string, byte[]>? _existingState;
         private readonly IDictionary<string, byte[]> _currentState;
-        private readonly List<OnPersistingCallback> _registeredCallbacks;
 
-        internal ComponentApplicationState(
+        private readonly List<Func<Task>> _registeredCallbacks;
+
+        internal PersistentComponentState(
             IDictionary<string, byte[]> currentState,
-            List<OnPersistingCallback> pauseCallbacks)
+            List<Func<Task>> pauseCallbacks)
         {
             _currentState = currentState;
             _registeredCallbacks = pauseCallbacks;
         }
 
+        internal bool PersistingState { get; set; }
+
         internal void InitializeExistingState(IDictionary<string, byte[]> existingState)
         {
             if (_existingState != null)
             {
-                throw new InvalidOperationException("ComponentApplicationState already initialized.");
+                throw new InvalidOperationException("PersistentComponentState already initialized.");
             }
             _existingState = existingState ?? throw new ArgumentNullException(nameof(existingState));
         }
 
         /// <summary>
-        /// Represents the method that performs operations when <see cref="OnPersisting"/> is raised and the application is about to be paused.
-        /// </summary>
-        /// <returns>A <see cref="Task"/> that will complete when the method is done preparing for the application pause.</returns>
-        public delegate Task OnPersistingCallback();
-
-        /// <summary>
-        /// An event that is raised when the application is about to be paused.
-        /// Registered handlers can use this opportunity to persist their state so that it can be retrieved when the application resumes.
+        /// Register a callback to persist the component state when the application is about to be paused.
+        /// Registered callbacks can use this opportunity to persist their state so that it can be retrieved when the application resumes.
         /// </summary>
-        public event OnPersistingCallback OnPersisting
+        /// <param name="callback">The callback to invoke when the application is being paused.</param>
+        /// <returns>A subscription that can be used to unregister the callback when disposed.</returns>
+        public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> callback)
         {
-            add
+            if (callback == null)
             {
-                if (value == null)
-                {
-                    throw new ArgumentNullException(nameof(value));
-                }
-
-                _registeredCallbacks.Add(value);
+                throw new ArgumentNullException(nameof(callback));
             }
-            remove
-            {
-                if (value == null)
-                {
-                    throw new ArgumentNullException(nameof(value));
-                }
 
-                _registeredCallbacks.Remove(value);
-            }
+            _registeredCallbacks.Add(callback);
+
+            return new PersistingComponentStateSubscription(_registeredCallbacks, callback);
         }
 
         /// <summary>
-        /// Tries to retrieve the persisted state with the given <paramref name="key"/>.
-        /// When the key is present, the state is successfully returned via <paramref name="value"/>
-        /// and removed from the <see cref="ComponentApplicationState"/>.
+        /// Serializes <paramref name="instance"/> as JSON and persists it under the given <paramref name="key"/>.
         /// </summary>
-        /// <param name="key">The key used to persist the state.</param>
-        /// <param name="value">The persisted state.</param>
-        /// <returns><c>true</c> if the state was found; <c>false</c> otherwise.</returns>
-        public bool TryTakePersistedState(string key, [MaybeNullWhen(false)] out byte[]? value)
+        /// <typeparam name="TValue">The <paramref name="instance"/> type.</typeparam>
+        /// <param name="key">The key to use to persist the state.</param>
+        /// <param name="instance">The instance to persist.</param>
+        [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")]
+        public void PersistAsJson<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string key, TValue instance)
         {
             if (key is null)
             {
                 throw new ArgumentNullException(nameof(key));
             }
 
-            if (_existingState == null)
+            if (key is null)
             {
-                // Services during prerendering might try to access their state upon injection on the page
-                // and we don't want to fail in that case.
-                // When a service is prerendering there is no state to restore and in other cases the host
-                // is responsible for initializing the state before services or components can access it.
-                value = null;
-                return false;
+                throw new ArgumentNullException(nameof(key));
             }
 
-            if (_existingState.TryGetValue(key, out value))
+            if (!PersistingState)
             {
-                _existingState.Remove(key);
-                return true;
+                throw new InvalidOperationException("Persisting state is only allowed during an OnPersisting callback.");
             }
-            else
+
+            if (_currentState.ContainsKey(key))
             {
-                return false;
+                throw new ArgumentException($"There is already a persisted object under the same key '{key}'");
             }
+
+            _currentState.Add(key, JsonSerializer.SerializeToUtf8Bytes(instance, JsonSerializerOptionsProvider.Options));
         }
 
         /// <summary>
-        /// Persists the serialized state <paramref name="value"/> for the given <paramref name="key"/>.
+        /// Tries to retrieve the persisted state as JSON with the given <paramref name="key"/> and deserializes it into an
+        /// instance of type <typeparamref name="TValue"/>.
+        /// When the key is present, the state is successfully returned via <paramref name="instance"/>
+        /// and removed from the <see cref="PersistentComponentState"/>.
         /// </summary>
-        /// <param name="key">The key to use to persist the state.</param>
-        /// <param name="value">The state to persist.</param>
-        public void PersistState(string key, byte[] value)
+        /// <param name="key">The key used to persist the instance.</param>
+        /// <param name="instance">The persisted instance.</param>
+        /// <returns><c>true</c> if the state was found; <c>false</c> otherwise.</returns>
+        [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")]
+        public bool TryTakeFromJson<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string key, [MaybeNullWhen(false)] out TValue? instance)
         {
             if (key is null)
             {
                 throw new ArgumentNullException(nameof(key));
             }
 
-            if (value is null)
+            if (TryTake(key, out var data))
             {
-                throw new ArgumentNullException(nameof(value));
+                var reader = new Utf8JsonReader(data);
+                instance = JsonSerializer.Deserialize<TValue>(ref reader, JsonSerializerOptionsProvider.Options)!;
+                return true;
             }
-
-            if (_currentState.ContainsKey(key))
+            else
             {
-                throw new ArgumentException($"There is already a persisted object under the same key '{key}'");
+                instance = default;
+                return false;
             }
-            _currentState.Add(key, value);
         }
 
-        /// <summary>
-        /// Serializes <paramref name="instance"/> as JSON and persists it under the given <paramref name="key"/>.
-        /// </summary>
-        /// <typeparam name="TValue">The <paramref name="instance"/> type.</typeparam>
-        /// <param name="key">The key to use to persist the state.</param>
-        /// <param name="instance">The instance to persist.</param>
-        [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")]
-        public void PersistAsJson<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string key, TValue instance)
+        private bool TryTake(string key, out byte[]? value)
         {
             if (key is null)
             {
                 throw new ArgumentNullException(nameof(key));
             }
 
-            PersistState(key, JsonSerializer.SerializeToUtf8Bytes(instance, JsonSerializerOptionsProvider.Options));
-        }
-
-        /// <summary>
-        /// Tries to retrieve the persisted state as JSON with the given <paramref name="key"/> and deserializes it into an
-        /// instance of type <typeparamref name="TValue"/>.
-        /// When the key is present, the state is successfully returned via <paramref name="instance"/>
-        /// and removed from the <see cref="ComponentApplicationState"/>.
-        /// </summary>
-        /// <param name="key">The key used to persist the instance.</param>
-        /// <param name="instance">The persisted instance.</param>
-        /// <returns><c>true</c> if the state was found; <c>false</c> otherwise.</returns>
-        [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")]
-        public bool TryTakeAsJson<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string key, [MaybeNullWhen(false)] out TValue? instance)
-        {
-            if (key is null)
+            if (_existingState == null)
             {
-                throw new ArgumentNullException(nameof(key));
+                // Services during prerendering might try to access their state upon injection on the page
+                // and we don't want to fail in that case.
+                // When a service is prerendering there is no state to restore and in other cases the host
+                // is responsible for initializing the state before services or components can access it.
+                value = default;
+                return false;
             }
 
-            if (TryTakePersistedState(key, out var data))
+            if (_existingState.TryGetValue(key, out value))
             {
-                instance = JsonSerializer.Deserialize<TValue>(data, JsonSerializerOptionsProvider.Options)!;
+                _existingState.Remove(key);
                 return true;
             }
             else
             {
-                instance = default(TValue);
                 return false;
             }
         }

+ 32 - 0
src/Components/Components/src/PersistingComponentStateSubscription.cs

@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Components.Infrastructure;
+
+namespace Microsoft.AspNetCore.Components
+{
+    /// <summary>
+    /// Represents a subscription to the <c>OnPersisting</c> callback that <see cref="ComponentStatePersistenceManager"/> callback will trigger
+    /// when the application is being persisted.
+    /// </summary>
+    public readonly struct PersistingComponentStateSubscription : IDisposable
+    {
+        private readonly List<Func<Task>>? _callbacks;
+        private readonly Func<Task>? _callback;
+
+        internal PersistingComponentStateSubscription(List<Func<Task>> callbacks, Func<Task> callback)
+        {
+            _callbacks = callbacks;
+            _callback = callback;
+        }
+
+        /// <inheritdoc />
+        public void Dispose()
+        {
+            if (_callback != null)
+            {
+                _callbacks?.Remove(_callback);
+            }
+        }
+    }
+}

+ 14 - 15
src/Components/Components/src/PublicAPI.Unshipped.txt

@@ -4,13 +4,6 @@
 *REMOVED*readonly Microsoft.AspNetCore.Components.RenderTree.RenderTreeEdit.RemovedAttributeName -> string!
 *REMOVED*Microsoft.AspNetCore.Components.NavigationManager.NavigateTo(string! uri, bool forceLoad = false) -> void
 *REMOVED*abstract Microsoft.AspNetCore.Components.NavigationManager.NavigateToCore(string! uri, bool forceLoad) -> void
-Microsoft.AspNetCore.Components.ComponentApplicationState
-Microsoft.AspNetCore.Components.ComponentApplicationState.OnPersisting -> Microsoft.AspNetCore.Components.ComponentApplicationState.OnPersistingCallback!
-Microsoft.AspNetCore.Components.ComponentApplicationState.OnPersistingCallback
-Microsoft.AspNetCore.Components.ComponentApplicationState.PersistAsJson<TValue>(string! key, TValue instance) -> void
-Microsoft.AspNetCore.Components.ComponentApplicationState.PersistState(string! key, byte[]! value) -> void
-Microsoft.AspNetCore.Components.ComponentApplicationState.TryTakeAsJson<TValue>(string! key, out TValue? instance) -> bool
-Microsoft.AspNetCore.Components.ComponentApplicationState.TryTakePersistedState(string! key, out byte[]? value) -> bool
 Microsoft.AspNetCore.Components.EditorRequiredAttribute
 Microsoft.AspNetCore.Components.EditorRequiredAttribute.EditorRequiredAttribute() -> void
 Microsoft.AspNetCore.Components.ErrorBoundaryBase
@@ -23,14 +16,8 @@ Microsoft.AspNetCore.Components.ErrorBoundaryBase.ErrorContent.set -> void
 Microsoft.AspNetCore.Components.ErrorBoundaryBase.MaximumErrorCount.get -> int
 Microsoft.AspNetCore.Components.ErrorBoundaryBase.MaximumErrorCount.set -> void
 Microsoft.AspNetCore.Components.ErrorBoundaryBase.Recover() -> void
-Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime
-Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime.ComponentApplicationLifetime(Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime!>! logger) -> void
-Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime.PersistStateAsync(Microsoft.AspNetCore.Components.Lifetime.IComponentApplicationStateStore! store, Microsoft.AspNetCore.Components.RenderTree.Renderer! renderer) -> System.Threading.Tasks.Task!
-Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime.RestoreStateAsync(Microsoft.AspNetCore.Components.Lifetime.IComponentApplicationStateStore! store) -> System.Threading.Tasks.Task!
-Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime.State.get -> Microsoft.AspNetCore.Components.ComponentApplicationState!
-Microsoft.AspNetCore.Components.Lifetime.IComponentApplicationStateStore
-Microsoft.AspNetCore.Components.Lifetime.IComponentApplicationStateStore.GetPersistedStateAsync() -> System.Threading.Tasks.Task<System.Collections.Generic.IDictionary<string!, byte[]!>!>!
-Microsoft.AspNetCore.Components.Lifetime.IComponentApplicationStateStore.PersistStateAsync(System.Collections.Generic.IReadOnlyDictionary<string!, byte[]!>! state) -> System.Threading.Tasks.Task!
+Microsoft.AspNetCore.Components.IPersistentComponentStateStore.GetPersistedStateAsync() -> System.Threading.Tasks.Task<System.Collections.Generic.IDictionary<string!, byte[]!>!>!
+Microsoft.AspNetCore.Components.IPersistentComponentStateStore.PersistStateAsync(System.Collections.Generic.IReadOnlyDictionary<string!, byte[]!>! state) -> System.Threading.Tasks.Task!
 Microsoft.AspNetCore.Components.NavigationManager.NavigateTo(string! uri, Microsoft.AspNetCore.Components.NavigationOptions options) -> void
 Microsoft.AspNetCore.Components.NavigationManager.NavigateTo(string! uri, bool forceLoad = false, bool replace = false) -> void
 Microsoft.AspNetCore.Components.NavigationManager.NavigateTo(string! uri, bool forceLoad) -> void
@@ -43,6 +30,18 @@ Microsoft.AspNetCore.Components.NavigationOptions.ReplaceHistoryEntry.get -> boo
 Microsoft.AspNetCore.Components.NavigationOptions.ReplaceHistoryEntry.init -> void
 Microsoft.AspNetCore.Components.RenderHandle.IsRenderingOnMetadataUpdate.get -> bool
 Microsoft.AspNetCore.Components.RenderTree.Renderer.RemoveRootComponent(int componentId) -> void
+Microsoft.AspNetCore.Components.IPersistentComponentStateStore
+Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager
+Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.ComponentStatePersistenceManager(Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager!>! logger) -> void
+Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.PersistStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.RenderTree.Renderer! renderer) -> System.Threading.Tasks.Task!
+Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.RestoreStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store) -> System.Threading.Tasks.Task!
+Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.State.get -> Microsoft.AspNetCore.Components.PersistentComponentState!
+Microsoft.AspNetCore.Components.PersistentComponentState
+Microsoft.AspNetCore.Components.PersistentComponentState.PersistAsJson<TValue>(string! key, TValue instance) -> void
+Microsoft.AspNetCore.Components.PersistentComponentState.RegisterOnPersisting(System.Func<System.Threading.Tasks.Task!>! callback) -> Microsoft.AspNetCore.Components.PersistingComponentStateSubscription
+Microsoft.AspNetCore.Components.PersistentComponentState.TryTakeFromJson<TValue>(string! key, out TValue? instance) -> bool
+Microsoft.AspNetCore.Components.PersistingComponentStateSubscription
+Microsoft.AspNetCore.Components.PersistingComponentStateSubscription.Dispose() -> void
 Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder.Dispose() -> void
 Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.get -> bool
 Microsoft.AspNetCore.Components.Routing.Router.PreferExactMatches.set -> void

+ 69 - 26
src/Components/Components/test/Lifetime/ComponentApplicationLifetimeTest.cs

@@ -2,9 +2,12 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.Buffers;
 using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components.Lifetime;
+using Microsoft.AspNetCore.Components.Infrastructure;
 using Microsoft.AspNetCore.Components.RenderTree;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
@@ -20,19 +23,19 @@ namespace Microsoft.AspNetCore.Components
         public async Task RestoreStateAsync_InitializesStateWithDataFromTheProvidedStore()
         {
             // Arrange
-            byte[] data = new byte[] { 0, 1, 2, 3, 4 };
+            var data = new byte[] { 0, 1, 2, 3, 4 };
             var state = new Dictionary<string, byte[]>
             {
-                ["MyState"] = data
+                ["MyState"] = JsonSerializer.SerializeToUtf8Bytes(data)
             };
             var store = new TestStore(state);
-            var lifetime = new ComponentApplicationLifetime(NullLogger<ComponentApplicationLifetime>.Instance);
+            var lifetime = new ComponentStatePersistenceManager(NullLogger<ComponentStatePersistenceManager>.Instance);
 
             // Act
             await lifetime.RestoreStateAsync(store);
 
             // Assert
-            Assert.True(lifetime.State.TryTakePersistedState("MyState", out var retrieved));
+            Assert.True(lifetime.State.TryTakeFromJson<byte[]>("MyState", out var retrieved));
             Assert.Empty(state);
             Assert.Equal(data, retrieved);
         }
@@ -46,7 +49,7 @@ namespace Microsoft.AspNetCore.Components
                 ["MyState"] = new byte[] { 0, 1, 2, 3, 4 }
             };
             var store = new TestStore(state);
-            var lifetime = new ComponentApplicationLifetime(NullLogger<ComponentApplicationLifetime>.Instance);
+            var lifetime = new ComponentStatePersistenceManager(NullLogger<ComponentStatePersistenceManager>.Instance);
 
             await lifetime.RestoreStateAsync(store);
 
@@ -60,19 +63,23 @@ namespace Microsoft.AspNetCore.Components
             // Arrange
             var state = new Dictionary<string, byte[]>();
             var store = new TestStore(state);
-            var lifetime = new ComponentApplicationLifetime(NullLogger<ComponentApplicationLifetime>.Instance);
+            var lifetime = new ComponentStatePersistenceManager(NullLogger<ComponentStatePersistenceManager>.Instance);
 
             var renderer = new TestRenderer();
             var data = new byte[] { 1, 2, 3, 4 };
 
-            lifetime.State.PersistState("MyState", new byte[] { 1, 2, 3, 4 });
+            lifetime.State.RegisterOnPersisting(() =>
+            {
+                lifetime.State.PersistAsJson("MyState", new byte[] { 1, 2, 3, 4 });
+                return Task.CompletedTask;
+            });
 
             // Act
             await lifetime.PersistStateAsync(store, renderer);
 
             // Assert
             Assert.True(store.State.TryGetValue("MyState", out var persisted));
-            Assert.Equal(data, persisted);
+            Assert.Equal(data, JsonSerializer.Deserialize<byte[]>(persisted.ToArray()));
         }
 
         [Fact]
@@ -81,12 +88,12 @@ namespace Microsoft.AspNetCore.Components
             // Arrange
             var state = new Dictionary<string, byte[]>();
             var store = new TestStore(state);
-            var lifetime = new ComponentApplicationLifetime(NullLogger<ComponentApplicationLifetime>.Instance);
+            var lifetime = new ComponentStatePersistenceManager(NullLogger<ComponentStatePersistenceManager>.Instance);
             var renderer = new TestRenderer();
             var data = new byte[] { 1, 2, 3, 4 };
             var invoked = false;
 
-            lifetime.State.OnPersisting += () => { invoked = true; return default; };
+            lifetime.State.RegisterOnPersisting(() => { invoked = true; return default; });
 
             // Act
             await lifetime.PersistStateAsync(store, renderer);
@@ -101,7 +108,7 @@ namespace Microsoft.AspNetCore.Components
             // Arrange
             var state = new Dictionary<string, byte[]>();
             var store = new TestStore(state);
-            var lifetime = new ComponentApplicationLifetime(NullLogger<ComponentApplicationLifetime>.Instance);
+            var lifetime = new ComponentStatePersistenceManager(NullLogger<ComponentStatePersistenceManager>.Instance);
             var renderer = new TestRenderer();
 
             var sequence = new List<int> { };
@@ -109,8 +116,8 @@ namespace Microsoft.AspNetCore.Components
             var tcs = new TaskCompletionSource();
             var tcs2 = new TaskCompletionSource();
 
-            lifetime.State.OnPersisting += async () => { sequence.Add(1); await tcs.Task; sequence.Add(3); };
-            lifetime.State.OnPersisting += async () => { sequence.Add(2); await tcs2.Task; sequence.Add(4); };
+            lifetime.State.RegisterOnPersisting(async () => { sequence.Add(1); await tcs.Task; sequence.Add(3); });
+            lifetime.State.RegisterOnPersisting(async () => { sequence.Add(2); await tcs2.Task; sequence.Add(4); });
 
             // Act
             var persistTask = lifetime.PersistStateAsync(store, renderer);
@@ -123,22 +130,53 @@ namespace Microsoft.AspNetCore.Components
             Assert.Equal(new[] { 1, 2, 3, 4 }, sequence);
         }
 
+        [Fact]
+        public async Task PersistStateAsync_CallbacksAreRemovedWhenSubscriptionsAreDisposed()
+        {
+            // Arrange
+            var state = new Dictionary<string, byte[]>();
+            var store = new TestStore(state);
+            var lifetime = new ComponentStatePersistenceManager(NullLogger<ComponentStatePersistenceManager>.Instance);
+            var renderer = new TestRenderer();
+
+            var sequence = new List<int> { };
+
+            var tcs = new TaskCompletionSource();
+            var tcs2 = new TaskCompletionSource();
+
+            var subscription1 = lifetime.State.RegisterOnPersisting(async () => { sequence.Add(1); await tcs.Task; sequence.Add(3); });
+            var subscription2 = lifetime.State.RegisterOnPersisting(async () => { sequence.Add(2); await tcs2.Task; sequence.Add(4); });
+
+            // Act
+            subscription1.Dispose();
+            subscription2.Dispose();
+
+            var persistTask = lifetime.PersistStateAsync(store, renderer);
+            tcs.SetResult();
+            tcs2.SetResult();
+
+            await persistTask;
+
+            // Assert
+            Assert.Empty(sequence);
+        }
+
         [Fact]
         public async Task PersistStateAsync_ContinuesInvokingPauseCallbacksDuringPersistIfACallbackThrows()
         {
             // Arrange
             var sink = new TestSink();
             var loggerFactory = new TestLoggerFactory(sink, true);
-            var logger = loggerFactory.CreateLogger<ComponentApplicationLifetime>();
+            var logger = loggerFactory.CreateLogger<ComponentStatePersistenceManager>();
             var state = new Dictionary<string, byte[]>();
             var store = new TestStore(state);
-            var lifetime = new ComponentApplicationLifetime(logger);
+            var lifetime = new ComponentStatePersistenceManager(logger);
             var renderer = new TestRenderer();
             var data = new byte[] { 1, 2, 3, 4 };
             var invoked = false;
 
-            lifetime.State.OnPersisting += () => throw new InvalidOperationException();
-            lifetime.State.OnPersisting += () => { invoked = true; return Task.CompletedTask; };
+            lifetime.State.RegisterOnPersisting(() => throw new InvalidOperationException());
+            lifetime.State.RegisterOnPersisting(() => { invoked = true; return Task.CompletedTask; });
 
             // Act
             await lifetime.PersistStateAsync(store, renderer);
@@ -155,16 +193,16 @@ namespace Microsoft.AspNetCore.Components
             // Arrange
             var sink = new TestSink();
             var loggerFactory = new TestLoggerFactory(sink, true);
-            var logger = loggerFactory.CreateLogger<ComponentApplicationLifetime>();
+            var logger = loggerFactory.CreateLogger<ComponentStatePersistenceManager>();
             var state = new Dictionary<string, byte[]>();
             var store = new TestStore(state);
-            var lifetime = new ComponentApplicationLifetime(logger);
+            var lifetime = new ComponentStatePersistenceManager(logger);
             var renderer = new TestRenderer();
             var invoked = false;
             var tcs = new TaskCompletionSource();
 
-            lifetime.State.OnPersisting += async () => { await tcs.Task; throw new InvalidOperationException(); };
-            lifetime.State.OnPersisting += () => { invoked = true; return Task.CompletedTask; };
+            lifetime.State.RegisterOnPersisting(async () => { await tcs.Task; throw new InvalidOperationException(); });
+            lifetime.State.RegisterOnPersisting(() => { invoked = true; return Task.CompletedTask; });
 
             // Act
             var persistTask = lifetime.PersistStateAsync(store, renderer);
@@ -184,12 +222,16 @@ namespace Microsoft.AspNetCore.Components
             // Arrange
             var state = new Dictionary<string, byte[]>();
             var store = new TestStore(state);
-            var lifetime = new ComponentApplicationLifetime(NullLogger<ComponentApplicationLifetime>.Instance);
+            var lifetime = new ComponentStatePersistenceManager(NullLogger<ComponentStatePersistenceManager>.Instance);
 
             var renderer = new TestRenderer();
             var data = new byte[] { 1, 2, 3, 4 };
 
-            lifetime.State.PersistState("MyState", new byte[] { 1, 2, 3, 4 });
+            lifetime.State.RegisterOnPersisting(() =>
+            {
+                lifetime.State.PersistAsJson<byte[]>("MyState", new byte[] { 1, 2, 3, 4 });
+                return Task.CompletedTask;
+            });
 
             // Act
             await lifetime.PersistStateAsync(store, renderer);
@@ -219,7 +261,7 @@ namespace Microsoft.AspNetCore.Components
             }
         }
 
-        private class TestStore : IComponentApplicationStateStore
+        private class TestStore : IPersistentComponentStateStore
         {
             public TestStore(IDictionary<string, byte[]> initialState)
             {
@@ -235,7 +277,8 @@ namespace Microsoft.AspNetCore.Components
 
             public Task PersistStateAsync(IReadOnlyDictionary<string, byte[]> state)
             {
-                State = new Dictionary<string, byte[]>(state);
+                // We copy the data here because it's no longer available after this call completes.
+                State = state.ToDictionary(k => k.Key, v => v.Value);
                 return Task.CompletedTask;
             }
         }

+ 35 - 27
src/Components/Components/test/Lifetime/ComponentApplicationStateTest.cs

@@ -2,9 +2,13 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.Buffers;
 using System.Collections.Generic;
 using System.Text.Json;
 using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components.Infrastructure;
+using Microsoft.AspNetCore.Components.Test.Helpers;
+using Microsoft.Extensions.Logging.Abstractions;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Components
@@ -15,17 +19,17 @@ namespace Microsoft.AspNetCore.Components
         public void InitializeExistingState_SetupsState()
         {
             // Arrange
-            var applicationState = new ComponentApplicationState(new Dictionary<string, byte[]>(), new List<ComponentApplicationState.OnPersistingCallback>());
+            var applicationState = new PersistentComponentState(new Dictionary<string, byte[]>(), new List<Func<Task>>());
             var existingState = new Dictionary<string, byte[]>
             {
-                ["MyState"] = new byte[] { 1, 2, 3, 4 }
+                ["MyState"] = JsonSerializer.SerializeToUtf8Bytes(new byte[] { 1, 2, 3, 4 })
             };
 
             // Act
             applicationState.InitializeExistingState(existingState);
 
             // Assert
-            Assert.True(applicationState.TryTakePersistedState("MyState", out var existing));
+            Assert.True(applicationState.TryTakeFromJson<byte[]>("MyState", out var existing));
             Assert.Equal(new byte[] { 1, 2, 3, 4 }, existing);
         }
 
@@ -33,7 +37,7 @@ namespace Microsoft.AspNetCore.Components
         public void InitializeExistingState_ThrowsIfAlreadyInitialized()
         {
             // Arrange
-            var applicationState = new ComponentApplicationState(new Dictionary<string, byte[]>(), new List<ComponentApplicationState.OnPersistingCallback>());
+            var applicationState = new PersistentComponentState(new Dictionary<string, byte[]>(), new List<Func<Task>>());
             var existingState = new Dictionary<string, byte[]>
             {
                 ["MyState"] = new byte[] { 1, 2, 3, 4 }
@@ -49,35 +53,36 @@ namespace Microsoft.AspNetCore.Components
         public void TryRetrieveState_ReturnsStateWhenItExists()
         {
             // Arrange
-            var applicationState = new ComponentApplicationState(new Dictionary<string, byte[]>(), new List<ComponentApplicationState.OnPersistingCallback>());
+            var applicationState = new PersistentComponentState(new Dictionary<string, byte[]>(), new List<Func<Task>>());
             var existingState = new Dictionary<string, byte[]>
             {
-                ["MyState"] = new byte[] { 1, 2, 3, 4 }
+                ["MyState"] = JsonSerializer.SerializeToUtf8Bytes(new byte[] { 1, 2, 3, 4 })
             };
 
             // Act
             applicationState.InitializeExistingState(existingState);
 
             // Assert
-            Assert.True(applicationState.TryTakePersistedState("MyState", out var existing));
+            Assert.True(applicationState.TryTakeFromJson<byte[]>("MyState", out var existing));
             Assert.Equal(new byte[] { 1, 2, 3, 4 }, existing);
-            Assert.False(applicationState.TryTakePersistedState("MyState", out var gone));
+            Assert.False(applicationState.TryTakeFromJson<byte[]>("MyState", out var gone));
         }
 
         [Fact]
-        public void PersistState_SavesDataToTheStore()
+        public void PersistState_SavesDataToTheStoreAsync()
         {
             // Arrange
             var currentState = new Dictionary<string, byte[]>();
-            var applicationState = new ComponentApplicationState(currentState, new List<ComponentApplicationState.OnPersistingCallback>());
+            var applicationState = new PersistentComponentState(currentState, new List<Func<Task>>());
+            applicationState.PersistingState = true;
             var myState = new byte[] { 1, 2, 3, 4 };
 
             // Act
-            applicationState.PersistState("MyState", myState);
+            applicationState.PersistAsJson("MyState", myState);
 
             // Assert
             Assert.True(currentState.TryGetValue("MyState", out var stored));
-            Assert.Equal(myState, stored);
+            Assert.Equal(myState, JsonSerializer.Deserialize<byte[]>(stored));
         }
 
         [Fact]
@@ -85,21 +90,23 @@ namespace Microsoft.AspNetCore.Components
         {
             // Arrange
             var currentState = new Dictionary<string, byte[]>();
-            var applicationState = new ComponentApplicationState(currentState, new List<ComponentApplicationState.OnPersistingCallback>());
+            var applicationState = new PersistentComponentState(currentState, new List<Func<Task>>());
+            applicationState.PersistingState = true;
             var myState = new byte[] { 1, 2, 3, 4 };
 
-            applicationState.PersistState("MyState", myState);
+            applicationState.PersistAsJson("MyState", myState);
 
             // Act & Assert
-            Assert.Throws<ArgumentException>(() => applicationState.PersistState("MyState", myState));
+            Assert.Throws<ArgumentException>(() => applicationState.PersistAsJson("MyState", myState));
         }
 
         [Fact]
-        public void PersistAsJson_SerializesTheDataToJson()
+        public void PersistAsJson_SerializesTheDataToJsonAsync()
         {
             // Arrange
             var currentState = new Dictionary<string, byte[]>();
-            var applicationState = new ComponentApplicationState(currentState, new List<ComponentApplicationState.OnPersistingCallback>());
+            var applicationState = new PersistentComponentState(currentState, new List<Func<Task>>());
+            applicationState.PersistingState = true;
             var myState = new byte[] { 1, 2, 3, 4 };
 
             // Act
@@ -111,14 +118,15 @@ namespace Microsoft.AspNetCore.Components
         }
 
         [Fact]
-        public void PersistAsJson_NullValue()
+        public void PersistAsJson_NullValueAsync()
         {
             // Arrange
             var currentState = new Dictionary<string, byte[]>();
-            var applicationState = new ComponentApplicationState(currentState, new List<ComponentApplicationState.OnPersistingCallback>());
+            var applicationState = new PersistentComponentState(currentState, new List<Func<Task>>());
+            applicationState.PersistingState = true;
 
             // Act
-            applicationState.PersistAsJson<byte []>("MyState", null);
+            applicationState.PersistAsJson<byte[]>("MyState", null);
 
             // Assert
             Assert.True(currentState.TryGetValue("MyState", out var stored));
@@ -132,34 +140,34 @@ namespace Microsoft.AspNetCore.Components
             var myState = new byte[] { 1, 2, 3, 4 };
             var serialized = JsonSerializer.SerializeToUtf8Bytes(myState);
             var existingState = new Dictionary<string, byte[]>() { ["MyState"] = serialized };
-            var applicationState = new ComponentApplicationState(new Dictionary<string, byte[]>(), new List<ComponentApplicationState.OnPersistingCallback>());
+            var applicationState = new PersistentComponentState(new Dictionary<string, byte[]>(), new List<Func<Task>>());
 
             applicationState.InitializeExistingState(existingState);
 
             // Act
-            Assert.True(applicationState.TryTakeAsJson<byte []>("MyState", out var stored));
+            Assert.True(applicationState.TryTakeFromJson<byte[]>("MyState", out var stored));
 
             // Assert
             Assert.Equal(myState, stored);
-            Assert.False(applicationState.TryTakeAsJson<byte[]>("MyState", out _));
+            Assert.False(applicationState.TryTakeFromJson<byte[]>("MyState", out _));
         }
 
         [Fact]
         public void TryRetrieveFromJson_NullValue()
         {
             // Arrange
-            var serialized = JsonSerializer.SerializeToUtf8Bytes<byte []>(null);
+            var serialized = JsonSerializer.SerializeToUtf8Bytes<byte[]>(null);
             var existingState = new Dictionary<string, byte[]>() { ["MyState"] = serialized };
-            var applicationState = new ComponentApplicationState(new Dictionary<string, byte[]>(), new List<ComponentApplicationState.OnPersistingCallback>());
+            var applicationState = new PersistentComponentState(new Dictionary<string, byte[]>(), new List<Func<Task>>());
 
             applicationState.InitializeExistingState(existingState);
 
             // Act
-            Assert.True(applicationState.TryTakeAsJson<byte[]>("MyState", out var stored));
+            Assert.True(applicationState.TryTakeFromJson<byte[]>("MyState", out var stored));
 
             // Assert
             Assert.Null(stored);
-            Assert.False(applicationState.TryTakeAsJson<byte[]>("MyState", out _));
+            Assert.False(applicationState.TryTakeFromJson<byte[]>("MyState", out _));
         }
     }
 }

+ 4 - 2
src/Components/ComponentsNoDeps.slnf

@@ -36,6 +36,8 @@
       "src\\Components\\WebAssembly\\testassets\\Wasm.Authentication.Client\\Wasm.Authentication.Client.csproj",
       "src\\Components\\WebAssembly\\testassets\\Wasm.Authentication.Server\\Wasm.Authentication.Server.csproj",
       "src\\Components\\WebAssembly\\testassets\\Wasm.Authentication.Shared\\Wasm.Authentication.Shared.csproj",
+      "src\\Components\\WebAssembly\\testassets\\Wasm.Prerendered.Client\\Wasm.Prerendered.Client.csproj",
+      "src\\Components\\WebAssembly\\testassets\\Wasm.Prerendered.Server\\Wasm.Prerendered.Server.csproj",
       "src\\Components\\WebAssembly\\testassets\\WasmLinkerTest\\WasmLinkerTest.csproj",
       "src\\Components\\WebView\\Samples\\PhotinoPlatform\\src\\Microsoft.AspNetCore.Components.WebView.Photino.csproj",
       "src\\Components\\WebView\\Samples\\PhotinoPlatform\\testassets\\PhotinoTestApp\\PhotinoTestApp.csproj",
@@ -46,8 +48,8 @@
       "src\\Components\\benchmarkapps\\Wasm.Performance\\ConsoleHost\\Wasm.Performance.ConsoleHost.csproj",
       "src\\Components\\benchmarkapps\\Wasm.Performance\\Driver\\Wasm.Performance.Driver.csproj",
       "src\\Components\\benchmarkapps\\Wasm.Performance\\TestApp\\Wasm.Performance.TestApp.csproj",
-      "src\\Components\\test\\E2ETest\\Microsoft.AspNetCore.Components.E2ETests.csproj",
       "src\\Components\\test\\E2ETestMigration\\Microsoft.AspNetCore.Components.Migration.E2ETests.csproj",
+      "src\\Components\\test\\E2ETest\\Microsoft.AspNetCore.Components.E2ETests.csproj",
       "src\\Components\\test\\testassets\\BasicTestApp\\BasicTestApp.csproj",
       "src\\Components\\test\\testassets\\ComponentsApp.Server\\ComponentsApp.Server.csproj",
       "src\\Components\\test\\testassets\\GlobalizationWasmApp\\GlobalizationWasmApp.csproj",
@@ -55,4 +57,4 @@
       "src\\Components\\test\\testassets\\TestServer\\Components.TestServer.csproj"
     ]
   }
-}
+}

+ 3 - 3
src/Components/Server/src/Circuits/CircuitFactory.cs

@@ -6,7 +6,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Security.Claims;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components.Lifetime;
+using Microsoft.AspNetCore.Components.Infrastructure;
 using Microsoft.AspNetCore.Components.Routing;
 using Microsoft.AspNetCore.DataProtection;
 using Microsoft.Extensions.DependencyInjection;
@@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
             string baseUri,
             string uri,
             ClaimsPrincipal user,
-            IComponentApplicationStateStore store)
+            IPersistentComponentStateStore store)
         {
             var scope = _scopeFactory.CreateAsyncScope();
             var jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService<IJSRuntime>();
@@ -63,7 +63,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
                 navigationManager.Initialize(baseUri, uri);
             }
 
-            var appLifetime = scope.ServiceProvider.GetRequiredService<ComponentApplicationLifetime>();
+            var appLifetime = scope.ServiceProvider.GetRequiredService<ComponentStatePersistenceManager>();
             await appLifetime.RestoreStateAsync(store);
 
             var jsComponentInterop = new CircuitJSComponentInterop(_options);

+ 4 - 5
src/Components/Server/src/Circuits/CircuitHandleRegistry.cs

@@ -6,7 +6,6 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Security.Claims;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components.Lifetime;
 using Microsoft.AspNetCore.Components.Routing;
 using Microsoft.AspNetCore.DataProtection;
 using Microsoft.Extensions.DependencyInjection;
@@ -18,14 +17,14 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
 {
     internal sealed class CircuitHandleRegistry : ICircuitHandleRegistry
     {
-        public CircuitHandle GetCircuitHandle(IDictionary<object, object?>circuitHandles, object circuitKey)
+        public CircuitHandle GetCircuitHandle(IDictionary<object, object?> circuitHandles, object circuitKey)
         {
             if (circuitHandles.TryGetValue(circuitKey, out var circuitHandle))
             {
-                return (CircuitHandle) circuitHandle;
+                return (CircuitHandle)circuitHandle;
             }
 
-            return null;;
+            return null; ;
         }
 
         public CircuitHost GetCircuit(IDictionary<object, object?> circuitHandles, object circuitKey)
@@ -38,7 +37,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
             return null;
         }
 
-        public void SetCircuit(IDictionary<object, object?>   circuitHandles, object circuitKey, CircuitHost circuitHost)
+        public void SetCircuit(IDictionary<object, object?> circuitHandles, object circuitKey, CircuitHost circuitHost)
         {
             circuitHandles[circuitKey] = circuitHost?.Handle;
         }

+ 2 - 2
src/Components/Server/src/Circuits/ICircuitFactory.cs

@@ -6,7 +6,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Security.Claims;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components.Lifetime;
+using Microsoft.AspNetCore.Components.Infrastructure;
 using Microsoft.AspNetCore.Components.Routing;
 using Microsoft.AspNetCore.DataProtection;
 using Microsoft.Extensions.DependencyInjection;
@@ -24,6 +24,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
             string baseUri,
             string uri,
             ClaimsPrincipal user,
-            IComponentApplicationStateStore store);
+            IPersistentComponentStateStore store);
     }
 }

+ 4 - 4
src/Components/Server/src/Circuits/ICircuitHandleRegistry.cs

@@ -6,7 +6,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Security.Claims;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components.Lifetime;
+using Microsoft.AspNetCore.Components.Infrastructure;
 using Microsoft.AspNetCore.Components.Routing;
 using Microsoft.AspNetCore.DataProtection;
 using Microsoft.Extensions.DependencyInjection;
@@ -18,10 +18,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
 {
     internal interface ICircuitHandleRegistry
     {
-        CircuitHandle GetCircuitHandle(IDictionary<object, object?>  circuitHandles, object circuitKey);
+        CircuitHandle GetCircuitHandle(IDictionary<object, object?> circuitHandles, object circuitKey);
 
-        CircuitHost GetCircuit(IDictionary<object, object?>  circuitHandles, object circuitKey);
+        CircuitHost GetCircuit(IDictionary<object, object?> circuitHandles, object circuitKey);
 
-        void SetCircuit(IDictionary<object, object?>  circuitHandles, object circuitKey, CircuitHost circuitHost);
+        void SetCircuit(IDictionary<object, object?> circuitHandles, object circuitKey, CircuitHost circuitHost);
     }
 }

+ 1 - 1
src/Components/Server/src/Circuits/IServerComponentDeserializer.cs

@@ -6,7 +6,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Security.Claims;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components.Lifetime;
+using Microsoft.AspNetCore.Components.Infrastructure;
 using Microsoft.AspNetCore.Components.Routing;
 using Microsoft.AspNetCore.DataProtection;
 using Microsoft.Extensions.DependencyInjection;

+ 7 - 6
src/Components/Server/test/Circuits/ComponentHubTest.cs

@@ -7,7 +7,7 @@ using System.Text.Json;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components.Lifetime;
+using Microsoft.AspNetCore.Components.Infrastructure;
 using Microsoft.AspNetCore.Components.Server.Circuits;
 using Microsoft.AspNetCore.DataProtection;
 using Microsoft.AspNetCore.SignalR;
@@ -132,12 +132,12 @@ namespace Microsoft.AspNetCore.Components.Server
         {
             private bool circuitSet = false;
 
-            public CircuitHandle GetCircuitHandle(IDictionary<object, object>   circuitHandles, object circuitKey)
+            public CircuitHandle GetCircuitHandle(IDictionary<object, object> circuitHandles, object circuitKey)
             {
                 return null;
             }
 
-            public CircuitHost GetCircuit(IDictionary<object, object>   circuitHandles, object circuitKey)
+            public CircuitHost GetCircuit(IDictionary<object, object> circuitHandles, object circuitKey)
             {
                 if (circuitSet)
                 {
@@ -149,7 +149,7 @@ namespace Microsoft.AspNetCore.Components.Server
                 return null;
             }
 
-            public void SetCircuit(IDictionary<object, object>   circuitHandles, object circuitKey, CircuitHost circuitHost)
+            public void SetCircuit(IDictionary<object, object> circuitHandles, object circuitKey, CircuitHost circuitHost)
             {
                 circuitSet = true;
                 return;
@@ -171,7 +171,8 @@ namespace Microsoft.AspNetCore.Components.Server
             IServiceScopeFactory scopeFactory,
             ILoggerFactory loggerFactory,
             CircuitIdFactory circuitIdFactory,
-            IOptions<CircuitOptions> options) { }
+            IOptions<CircuitOptions> options)
+            { }
 
             // Implement a `CreateCircuitHostAsync` that mocks the construction
             // of the CircuitHost.
@@ -181,7 +182,7 @@ namespace Microsoft.AspNetCore.Components.Server
                 string baseUri,
                 string uri,
                 ClaimsPrincipal user,
-                IComponentApplicationStateStore store)
+                IPersistentComponentStateStore store)
             {
                 var serviceScope = new Mock<IServiceScope>();
                 var circuitHost = TestCircuitHost.Create(serviceScope: new AsyncServiceScope(serviceScope.Object));

+ 12 - 6
src/Components/WebAssembly/Samples/HostedBlazorWebassemblyApp/Client/Pages/FetchData.razor

@@ -2,7 +2,7 @@
 @implements IDisposable
 @using HostedBlazorWebassemblyApp.Shared
 @inject IWeatherForecastService WeatherForecastService
-@inject ComponentApplicationState ApplicationState
+@inject PersistentComponentState ApplicationState
 
 <h1>Weather forecast</h1>
 
@@ -39,13 +39,19 @@ else
 
 @code {
     private WeatherForecast[] forecasts = Array.Empty<WeatherForecast>();
+    private PersistingComponentStateSubscription _persistingSubscription;
 
     protected override async Task OnInitializedAsync()
     {
-        ApplicationState.OnPersisting += PersistForecasts;
-        forecasts = !ApplicationState.TryTakeAsJson<WeatherForecast[]>("fetchdata", out var restored) ?
-            await WeatherForecastService.GetForecastAsync(DateTime.Now) :
-            restored!;
+        _persistingSubscription = ApplicationState.RegisterOnPersisting(PersistForecasts);
+        if (!ApplicationState.TryTakeFromJson<WeatherForecast[]>("fetchdata", out var restored))
+        {
+            forecasts = await WeatherForecastService.GetForecastAsync(DateTime.Now);
+        }
+        else
+        {
+            forecasts = restored!;
+        }
     }
 
     private Task PersistForecasts()
@@ -56,6 +62,6 @@ else
 
     void IDisposable.Dispose()
     {
-        ApplicationState.OnPersisting -= PersistForecasts;
+        _persistingSubscription.Dispose();
     }
 }

+ 2 - 3
src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs

@@ -1,9 +1,8 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Diagnostics;
 using System.Reflection.Metadata;
-using Microsoft.AspNetCore.Components.Lifetime;
+using Microsoft.AspNetCore.Components.Infrastructure;
 using Microsoft.AspNetCore.Components.Web.Infrastructure;
 using Microsoft.AspNetCore.Components.WebAssembly.HotReload;
 using Microsoft.AspNetCore.Components.WebAssembly.Infrastructure;
@@ -129,7 +128,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
             // This is the earliest opportunity to fetch satellite assemblies for this selection.
             await cultureProvider.LoadCurrentCultureResourcesAsync();
 
-            var manager = Services.GetRequiredService<ComponentApplicationLifetime>();
+            var manager = Services.GetRequiredService<ComponentStatePersistenceManager>();
             var store = !string.IsNullOrEmpty(_persistedState) ?
                 new PrerenderComponentApplicationStore(_persistedState) :
                 new PrerenderComponentApplicationStore();

+ 4 - 3
src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs

@@ -5,8 +5,9 @@ using System;
 using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.IO;
+using System.Linq;
 using System.Text.Json;
-using Microsoft.AspNetCore.Components.Lifetime;
+using Microsoft.AspNetCore.Components.Infrastructure;
 using Microsoft.AspNetCore.Components.RenderTree;
 using Microsoft.AspNetCore.Components.Routing;
 using Microsoft.AspNetCore.Components.Web;
@@ -254,8 +255,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
             Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
             Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
             Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance));
-            Services.AddSingleton<ComponentApplicationLifetime>();
-            Services.AddSingleton<ComponentApplicationState>(sp => sp.GetRequiredService<ComponentApplicationLifetime>().State);
+            Services.AddSingleton<ComponentStatePersistenceManager>();
+            Services.AddSingleton<PersistentComponentState>(sp => sp.GetRequiredService<ComponentStatePersistenceManager>().State);
             Services.AddSingleton<IErrorBoundaryLogger, WebAssemblyErrorBoundaryLogger>();
             Services.AddLogging(builder =>
             {

+ 2 - 2
src/Components/test/E2ETest/Tests/CircuitTests.cs

@@ -8,11 +8,11 @@ using BasicTestApp;
 using BasicTestApp.Reconnection;
 using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
 using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
+using Microsoft.AspNetCore.Components.Infrastructure;
 using Microsoft.AspNetCore.E2ETesting;
 using OpenQA.Selenium;
 using TestServer;
 using Xunit;
-using Microsoft.AspNetCore.Components.Lifetime;
 using Xunit.Abstractions;
 
 namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
@@ -99,4 +99,4 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
             }
         }
     }
-}
+}

+ 27 - 9
src/Components/test/testassets/BasicTestApp/PreserveStateComponent.razor

@@ -1,6 +1,8 @@
-@inject ComponentApplicationState AppState
+@inject PersistentComponentState AppState
 @inject PreserveStateService PreserveService
+@implements IDisposable
 @using System.Text
+@using System.Buffers
 
 <p id="@($"state-{State}")">@State: @_state</p>
 
@@ -40,33 +42,44 @@
     private string _extraState = null;
     private bool _restored;
     private bool? _extraStateAvailable;
+    private PersistingComponentStateSubscription _registration;
 
     protected override void OnInitialized()
     {
-        if (AppState.TryTakePersistedState(State, out var preserved))
+        if (AppState.TryTakeFromJson<string>(State, out var preserved))
         {
-            _state = Encoding.UTF8.GetString(preserved);
+            _state = preserved;
             _restored = true;
         }
         else
         {
-            _state = Guid.NewGuid().ToString();
-            AppState.PersistState(State, Encoding.UTF8.GetBytes(_state));
-            _extraState = Guid.NewGuid().ToString();
             if(ExtraState != null)
             {
                 _extraStateAvailable = true;
-                AppState.PersistState(ExtraState, Encoding.UTF8.GetBytes(_extraState));
+            }
+            _state = Guid.NewGuid().ToString();
+            _extraState = Guid.NewGuid().ToString();
+            _registration = AppState.RegisterOnPersisting(PersistState);
+
+            Task PersistState()
+            {
+                AppState.PersistAsJson(State, _state);
+                if(ExtraState != null)
+                {
+                    AppState.PersistAsJson(ExtraState, _extraState);
+                }
+
+                return Task.CompletedTask;
             }
         }
     }
 
     public void DisplayExtraState()
     {
-        if (AppState.TryTakePersistedState(ExtraState, out var extraState))
+        if (AppState.TryTakeFromJson<string>(ExtraState, out var extraState))
         {
             _extraStateAvailable = true;
-            _extraState = Encoding.UTF8.GetString(extraState);
+            _extraState = extraState;
         }
         else
         {
@@ -74,4 +87,9 @@
             _extraState = Guid.NewGuid().ToString();
         }
     }
+
+    void IDisposable.Dispose()
+    {
+        _registration.Dispose();
+    }
 }

+ 6 - 8
src/Components/test/testassets/BasicTestApp/PreserveStateService.cs

@@ -9,14 +9,15 @@ namespace BasicTestApp
 {
     public class PreserveStateService : IDisposable
     {
-        private readonly ComponentApplicationState _componentApplicationState;
+        private readonly PersistentComponentState _componentApplicationState;
+        private PersistingComponentStateSubscription _persistingSubscription;
 
         private ServiceState _state = new();
 
-        public PreserveStateService(ComponentApplicationState componentApplicationState)
+        public PreserveStateService(PersistentComponentState componentApplicationState)
         {
             _componentApplicationState = componentApplicationState;
-            _componentApplicationState.OnPersisting += PersistState;
+            _persistingSubscription = _componentApplicationState.RegisterOnPersisting(PersistState);
             TryRestoreState();
         }
 
@@ -24,7 +25,7 @@ namespace BasicTestApp
 
         private void TryRestoreState()
         {
-            if (_componentApplicationState.TryTakeAsJson<ServiceState>("Service", out var state))
+            if (_componentApplicationState.TryTakeFromJson<ServiceState>("Service", out var state))
             {
                 _state = state;
             }
@@ -42,10 +43,7 @@ namespace BasicTestApp
             return Task.CompletedTask;
         }
 
-        public void Dispose()
-        {
-            _componentApplicationState.OnPersisting -= PersistState;
-        }
+        public void Dispose() => _persistingSubscription.Dispose();
 
         private class ServiceState
         {

+ 1 - 1
src/Components/test/testassets/TestServer/Properties/launchSettings.json

@@ -14,7 +14,7 @@
       "environmentVariables": {
         "ASPNETCORE_ENVIRONMENT": "Development"
       },
-      "applicationUrl": "https://localhost:5001;http://localhost:5000"
+      "applicationUrl": "https://localhost:5003;http://localhost:5002"
     },
     "IIS Express": {
       "commandName": "IISExpress",

+ 24 - 3
src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs

@@ -2,9 +2,11 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.IO;
+using System.Text.Encodings.Web;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components;
-using Microsoft.AspNetCore.Components.Lifetime;
+using Microsoft.AspNetCore.Components.Infrastructure;
 using Microsoft.AspNetCore.Components.Rendering;
 using Microsoft.AspNetCore.DataProtection;
 using Microsoft.AspNetCore.Html;
@@ -56,7 +58,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
             }
 
             var services = ViewContext.HttpContext.RequestServices;
-            var manager = services.GetRequiredService<ComponentApplicationLifetime>();
+            var manager = services.GetRequiredService<ComponentStatePersistenceManager>();
             var renderer = services.GetRequiredService<HtmlRenderer>();
             var store = PersistenceMode switch
             {
@@ -88,9 +90,28 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
                 output.Content.SetHtmlContent(
                     new HtmlContentBuilder()
                         .AppendHtml("<!--Blazor-Component-State:")
-                        .AppendHtml(store.PersistedState)
+                        .AppendHtml(new ComponentStateHtmlContent(store))
                         .AppendHtml("-->"));
             }
         }
+
+        private class ComponentStateHtmlContent : IHtmlContent
+        {
+            private PrerenderComponentApplicationStore _store;
+
+            public ComponentStateHtmlContent(PrerenderComponentApplicationStore store)
+            {
+                _store = store;
+            }
+
+            public void WriteTo(TextWriter writer, HtmlEncoder encoder)
+            {
+                if (_store != null)
+                {
+                    writer.Write(_store.PersistedState);
+                    _store = null;
+                }
+            }
+        }
     }
 }

+ 2 - 3
src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs

@@ -5,8 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.Text.Encodings.Web;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components;
-using Microsoft.AspNetCore.Components.Lifetime;
+using Microsoft.AspNetCore.Components.Infrastructure;
 using Microsoft.AspNetCore.Components.Rendering;
 using Microsoft.AspNetCore.DataProtection;
 using Microsoft.AspNetCore.Html;
@@ -191,7 +190,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
             {
                 RequestServices = new ServiceCollection()
                     .AddSingleton(renderer)
-                    .AddSingleton(new ComponentApplicationLifetime(NullLogger<ComponentApplicationLifetime>.Instance))
+                    .AddSingleton(new ComponentStatePersistenceManager(NullLogger<ComponentStatePersistenceManager>.Instance))
                     .AddSingleton<HtmlRenderer>()
                     .AddSingleton(_ephemeralProvider)
                     .AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance)

+ 8 - 4
src/Mvc/Mvc.TagHelpers/test/PrerenderComponentApplicationStoreTest.cs

@@ -2,7 +2,9 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.Buffers;
 using System.Collections.Generic;
+using System.Linq;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components;
 using Xunit;
@@ -19,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
             var store = new PrerenderComponentApplicationStore();
             var state = new Dictionary<string, byte[]>()
             {
-                ["MyValue"] = new byte[] {1,2,3,4}
+                ["MyValue"] = new byte[] { 1, 2, 3, 4 }
             };
 
             // Act
@@ -35,16 +37,18 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
             // Arrange
             var persistedState = "eyJNeVZhbHVlIjoiQVFJREJBPT0ifQ==";
             var store = new PrerenderComponentApplicationStore(persistedState);
-            var expected = new Dictionary<string, byte[]>()
+            var expected = new Dictionary<string, ReadOnlySequence<byte>>()
             {
-                ["MyValue"] = new byte[] { 1, 2, 3, 4 }
+                ["MyValue"] = new ReadOnlySequence<byte>(new byte[] { 1, 2, 3, 4 })
             };
 
             // Act
             var state = await store.GetPersistedStateAsync();
 
             // Assert
-            Assert.Equal(expected, state);
+            Assert.Equal(
+                expected.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToArray()),
+                state.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToArray()));
         }
     }
 }

+ 5 - 1
src/Mvc/Mvc.TagHelpers/test/ProtectedPrerenderComponentApplicationStateTest.cs

@@ -2,7 +2,9 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.Buffers;
 using System.Collections.Generic;
+using System.Linq;
 using System.Security.Cryptography;
 using System.Text.Json;
 using System.Threading.Tasks;
@@ -52,7 +54,9 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
             var restored = await store.GetPersistedStateAsync();
 
             // Assert
-            Assert.Equal(expectedState, restored);
+            Assert.Equal(
+                expectedState.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToArray()),
+                restored.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToArray()));
         }
 
         [Fact]

+ 3 - 3
src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs

@@ -5,7 +5,7 @@ using System;
 using System.Buffers;
 using System.Linq;
 using Microsoft.AspNetCore.Components;
-using Microsoft.AspNetCore.Components.Lifetime;
+using Microsoft.AspNetCore.Components.Infrastructure;
 using Microsoft.AspNetCore.Components.Rendering;
 using Microsoft.AspNetCore.Components.Routing;
 using Microsoft.AspNetCore.Components.Web;
@@ -239,8 +239,8 @@ namespace Microsoft.Extensions.DependencyInjection
             services.TryAddScoped<NavigationManager, HttpNavigationManager>();
             services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>();
             services.TryAddScoped<INavigationInterception, UnsupportedNavigationInterception>();
-            services.TryAddScoped<ComponentApplicationLifetime>();
-            services.TryAddScoped<ComponentApplicationState>(sp => sp.GetRequiredService<ComponentApplicationLifetime>().State);
+            services.TryAddScoped<ComponentStatePersistenceManager>();
+            services.TryAddScoped<PersistentComponentState>(sp => sp.GetRequiredService<ComponentStatePersistenceManager>().State);
             services.TryAddScoped<IErrorBoundaryLogger, PrerenderingErrorBoundaryLogger>();
 
             services.TryAddTransient<ControllerSaveTempDataPropertyFilter>();

+ 2 - 2
src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs

@@ -6,7 +6,7 @@ using System.Collections.Generic;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.Authorization;
-using Microsoft.AspNetCore.Components.Lifetime;
+using Microsoft.AspNetCore.Components.Infrastructure;
 using Microsoft.AspNetCore.Components.Rendering;
 using Microsoft.AspNetCore.Components.Routing;
 using Microsoft.AspNetCore.Http;
@@ -88,7 +88,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
 
                 // It's important that this is initialized since a component might try to restore state during prerendering
                 // (which will obviously not work, but should not fail)
-                var componentApplicationLifetime = httpContext.RequestServices.GetRequiredService<ComponentApplicationLifetime>();
+                var componentApplicationLifetime = httpContext.RequestServices.GetRequiredService<ComponentStatePersistenceManager>();
                 await componentApplicationLifetime.RestoreStateAsync(new PrerenderComponentApplicationStore());
             }
         }

+ 4 - 4
src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentRendererTest.cs

@@ -9,7 +9,7 @@ using System.Text.Json;
 using System.Text.RegularExpressions;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components;
-using Microsoft.AspNetCore.Components.Lifetime;
+using Microsoft.AspNetCore.Components.Infrastructure;
 using Microsoft.AspNetCore.Components.Rendering;
 using Microsoft.AspNetCore.DataProtection;
 using Microsoft.AspNetCore.Http;
@@ -881,9 +881,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
             services.AddSingleton<IJSRuntime, UnsupportedJavaScriptRuntime>();
             services.AddSingleton<NavigationManager, HttpNavigationManager>();
             services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
-            services.AddSingleton<ILogger<ComponentApplicationLifetime>, NullLogger<ComponentApplicationLifetime>>();
-            services.AddSingleton<ComponentApplicationLifetime>();
-            services.AddSingleton(sp => sp.GetRequiredService<ComponentApplicationLifetime>().State);
+            services.AddSingleton<ILogger<ComponentStatePersistenceManager>, NullLogger<ComponentStatePersistenceManager>>();
+            services.AddSingleton<ComponentStatePersistenceManager>();
+            services.AddSingleton(sp => sp.GetRequiredService<ComponentStatePersistenceManager>().State);
             return services;
         }
 

+ 107 - 0
src/Mvc/perf/Microbenchmarks/Microsoft.AspNetCore.Mvc/PreserveComponentStateBenchmark.cs

@@ -0,0 +1,107 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Security.Cryptography;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.AspNetCore.Mvc.TagHelpers;
+using Microsoft.AspNetCore.Razor.TagHelpers;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Microsoft.AspNetCore.Mvc.Microbenchmarks
+{
+    public class PreserveComponentStateBenchmark
+    {
+        private readonly PersistComponentStateTagHelper _tagHelper = new()
+        {
+            PersistenceMode = PersistenceMode.WebAssembly
+        };
+
+        TagHelperAttributeList _attributes = new();
+
+        private TagHelperContext _context;
+        private Func<bool, HtmlEncoder, Task<TagHelperContent>> _childContent =
+            (_, __) => Task.FromResult(new DefaultTagHelperContent() as TagHelperContent);
+        private IServiceProvider _serviceProvider;
+        private IServiceScope _serviceScope;
+        private TagHelperOutput _output;
+        private Dictionary<string, byte[]> _entries = new();
+
+        private byte[] _entryValue;
+
+        public PreserveComponentStateBenchmark()
+        {
+            _context = new TagHelperContext(_attributes, new Dictionary<object, object>(), "asdf");
+            _serviceProvider = new ServiceCollection()
+                .AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance)
+                .AddScoped(typeof(ILogger<>), typeof(NullLogger<>))
+                .AddMvc().Services.BuildServiceProvider();
+        }
+
+        // From 30 entries of about 100 bytes (~3K) to 100 entries with 100K per entry (~10MB)
+        // Sending 10MB of prerendered state is too much, and only used as a way to "stress" the system.
+        // In general, so long as entries don't exceed the buffer limits we are ok.
+        // 300 Kb is the upper limit of a reasonable payload for prerendered state
+        // The 8386 was selected by serializing 100 weather forecast records as a reference
+        // For regular runs we only enable by default 30 entries and 8386 bytes per entry, which is about 250K of serialized
+        // state on the limit of the accepted payload size budget for critical resources served from a page.
+        [Params(30 /*, 100*/)]
+        public int Entries;
+
+        [Params(/*100,*/ 8386/*, 100_000*/)]
+        public int EntrySize;
+
+        [GlobalSetup]
+        public void Setup()
+        {
+            _entryValue = new byte[EntrySize];
+            RandomNumberGenerator.Fill(_entryValue);
+            for (int i = 0; i < Entries; i++)
+            {
+                _entries.Add(i.ToString(CultureInfo.InvariantCulture), _entryValue);
+            }
+        }
+
+        [Benchmark(Description = "Persist component state tag helper webassembly")]
+        public async Task PersistComponentStateTagHelperWebAssemblyAsync()
+        {
+            _tagHelper.ViewContext = GetViewContext();
+            var state = _tagHelper.ViewContext.HttpContext.RequestServices.GetRequiredService<PersistentComponentState>();
+            foreach (var (key, value) in _entries)
+            {
+                state.PersistAsJson(key, value);
+            }
+
+            _output = new TagHelperOutput("persist-component-state", _attributes, _childContent);
+            _output.Content = new DefaultTagHelperContent();
+            await _tagHelper.ProcessAsync(_context, _output);
+            _output.Content.WriteTo(StreamWriter.Null, NullHtmlEncoder.Default);
+            _serviceScope.Dispose();
+        }
+
+        private ViewContext GetViewContext()
+        {
+            _serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
+            var httpContext = new DefaultHttpContext
+            {
+                RequestServices = _serviceScope.ServiceProvider
+            };
+
+            return new ViewContext
+            {
+                HttpContext = httpContext,
+            };
+        }
+    }
+}

+ 18 - 15
src/Shared/Components/PrerenderComponentApplicationStore.cs

@@ -1,17 +1,14 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System;
-using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.Text.Json;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components.Lifetime;
 
 namespace Microsoft.AspNetCore.Components
 {
-    internal class PrerenderComponentApplicationStore : IComponentApplicationStateStore
+    internal class PrerenderComponentApplicationStore : IPersistentComponentStateStore
     {
+
         public PrerenderComponentApplicationStore()
         {
             ExistingState = new();
@@ -25,8 +22,19 @@ namespace Microsoft.AspNetCore.Components
                 throw new ArgumentNullException(nameof(existingState));
             }
 
-            ExistingState = JsonSerializer.Deserialize<Dictionary<string, byte[]>>(Convert.FromBase64String(existingState)) ??
-                throw new ArgumentNullException(nameof(existingState));
+            DeserializeState(Convert.FromBase64String(existingState));
+        }
+
+        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Simple deserialize of primitive types.")]
+        protected void DeserializeState(byte[] existingState)
+        {
+            var state = JsonSerializer.Deserialize<Dictionary<string, byte[]>>(existingState);
+            if (state == null)
+            {
+                throw new ArgumentException("Could not deserialize state correctly", nameof(existingState));
+            }
+
+            ExistingState = state;
         }
 
 #nullable enable
@@ -41,17 +49,12 @@ namespace Microsoft.AspNetCore.Components
         }
 
         [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Simple serialize of primitive types.")]
-        protected virtual byte[] SerializeState(IReadOnlyDictionary<string, byte[]> state)
-        {
-            return JsonSerializer.SerializeToUtf8Bytes(state);
-        }
+        protected virtual byte[] SerializeState(IReadOnlyDictionary<string, byte[]> state) =>
+            JsonSerializer.SerializeToUtf8Bytes(state);
 
         public Task PersistStateAsync(IReadOnlyDictionary<string, byte[]> state)
         {
-            var bytes = SerializeState(state);
-
-            var result = Convert.ToBase64String(bytes);
-            PersistedState = result;
+            PersistedState = Convert.ToBase64String(SerializeState(state));
             return Task.CompletedTask;
         }
     }

+ 5 - 7
src/Shared/Components/ProtectedPrerenderComponentApplicationStore.cs

@@ -2,8 +2,11 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.Buffers;
 using System.Collections.Generic;
 using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components.Infrastructure;
 using Microsoft.AspNetCore.DataProtection;
 
 namespace Microsoft.AspNetCore.Components
@@ -20,18 +23,13 @@ namespace Microsoft.AspNetCore.Components
         public ProtectedPrerenderComponentApplicationStore(string existingState, IDataProtectionProvider dataProtectionProvider)
         {
             CreateProtector(dataProtectionProvider);
-            ExistingState = JsonSerializer.Deserialize<Dictionary<string, byte[]>>(_protector.Unprotect(Convert.FromBase64String(existingState)));
+            DeserializeState(_protector.Unprotect(Convert.FromBase64String(existingState)));
         }
 
         protected override byte[] SerializeState(IReadOnlyDictionary<string, byte[]> state)
         {
             var bytes = base.SerializeState(state);
-            if (_protector != null)
-            {
-                bytes = _protector.Protect(bytes);
-            }
-
-            return bytes;
+            return _protector != null ? _protector.Protect(bytes) : bytes;
         }
 
         private void CreateProtector(IDataProtectionProvider dataProtectionProvider) =>