diff --git a/src/Components/Components/src/Lifetime/IComponentApplicationStateStore.cs b/src/Components/Components/src/IPersistentComponentStateStore.cs
similarity index 69%
rename from src/Components/Components/src/Lifetime/IComponentApplicationStateStore.cs
rename to src/Components/Components/src/IPersistentComponentStateStore.cs
index 68312633c5c8..9b015dad6748 100644
--- a/src/Components/Components/src/Lifetime/IComponentApplicationStateStore.cs
+++ b/src/Components/Components/src/IPersistentComponentStateStore.cs
@@ -1,27 +1,26 @@
// 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;
+using System.Buffers;
-namespace Microsoft.AspNetCore.Components.Lifetime
+namespace Microsoft.AspNetCore.Components
{
///
/// Manages the storage for components and services that are part of a Blazor application.
///
- public interface IComponentApplicationStateStore
+ public interface IPersistentComponentStateStore
{
///
/// Gets the persisted state from the store.
///
/// The persisted state.
- Task> GetPersistedStateAsync();
+ Task>> GetPersistedStateAsync();
///
/// Persists the serialized state into the storage.
///
/// The serialized state to persist.
/// A that completes when the state is persisted to disk.
- Task PersistStateAsync(IReadOnlyDictionary state);
+ Task PersistStateAsync(IReadOnlyDictionary> state);
}
}
diff --git a/src/Components/Components/src/Lifetime/ComponentApplicationLifetime.cs b/src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs
similarity index 58%
rename from src/Components/Components/src/Lifetime/ComponentApplicationLifetime.cs
rename to src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs
index 13e4cd308081..3bfaf93b28fd 100644
--- a/src/Components/Components/src/Lifetime/ComponentApplicationLifetime.cs
+++ b/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
{
///
- /// Manages the lifetime of a component application.
+ /// Manages the persistent state of components in an application.
///
- public class ComponentApplicationLifetime
+ public class ComponentStatePersistenceManager : IDisposable
{
private bool _stateIsPersisted;
- private readonly List _pauseCallbacks = new();
- private readonly Dictionary _currentState = new();
- private readonly ILogger _logger;
+ private readonly List> _pauseCallbacks = new();
+ private readonly Dictionary _currentState = new(StringComparer.Ordinal);
+ private readonly ILogger _logger;
///
- /// Initializes a new instance of .
+ /// Initializes a new instance of .
///
- public ComponentApplicationLifetime(ILogger logger)
+ public ComponentStatePersistenceManager(ILogger logger)
{
- State = new ComponentApplicationState(_currentState, _pauseCallbacks);
+ State = new PersistentComponentState(_currentState, _pauseCallbacks);
_logger = logger;
}
///
- /// Gets the associated with the .
+ /// Gets the associated with the .
///
- public ComponentApplicationState State { get; }
+ public PersistentComponentState State { get; }
///
- /// Restores the component application state from the given .
+ /// Restores the component application state from the given .
///
- /// The to restore the application state from.
+ /// The to restore the application state from.
/// A that will complete when the state has been restored.
- public async Task RestoreStateAsync(IComponentApplicationStateStore store)
+ public async Task RestoreStateAsync(IPersistentComponentStateStore store)
{
var data = await store.GetPersistedStateAsync();
State.InitializeExistingState(data);
}
///
- /// Persists the component application state into the given .
+ /// Persists the component application state into the given .
///
- /// The to restore the application state from.
+ /// The to restore the application state from.
/// The that components are being rendered.
/// A that will complete when the state has been restored.
- public Task PersistStateAsync(IComponentApplicationStateStore store, Renderer renderer)
+ public Task PersistStateAsync(IPersistentComponentStateStore store, Renderer renderer)
{
if (_stateIsPersisted)
{
@@ -64,10 +68,23 @@ public Task PersistStateAsync(IComponentApplicationStateStore store, Renderer re
async Task PauseAndPersistState()
{
+ State.PersistingState = true;
await PauseAsync();
+ State.PersistingState = false;
+
+ var data = new Dictionary>(StringComparer.Ordinal);
+ foreach (var (key, value) in _currentState)
+ {
+ data[key] = new ReadOnlySequence(value.WrittenMemory);
+ }
- var data = new ReadOnlyDictionary(_currentState);
await store.PersistStateAsync(data);
+
+ foreach (var value in _currentState.Values)
+ {
+ value.Dispose();
+ }
+ _currentState.Clear();
}
}
@@ -75,7 +92,7 @@ internal Task PauseAsync()
{
List? 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 +112,7 @@ internal Task PauseAsync()
return Task.CompletedTask;
}
- static Task ExecuteCallback(ComponentApplicationState.OnPersistingCallback callback, ILogger logger)
+ static Task ExecuteCallback(Func callback, ILogger logger)
{
try
{
@@ -115,7 +132,7 @@ static Task ExecuteCallback(ComponentApplicationState.OnPersistingCallback callb
return Task.CompletedTask;
}
- static async Task Awaited(Task task, ILogger logger)
+ static async Task Awaited(Task task, ILogger logger)
{
try
{
@@ -129,5 +146,14 @@ static async Task Awaited(Task task, ILogger logge
}
}
}
+
+ void IDisposable.Dispose()
+ {
+ foreach (var value in _currentState.Values)
+ {
+ value.Dispose();
+ }
+ _currentState.Clear();
+ }
}
}
diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
index f68c4e4bfcf1..0c1e2d985d6f 100644
--- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
+++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
@@ -15,6 +15,7 @@
+
diff --git a/src/Components/Components/src/Lifetime/ComponentApplicationState.cs b/src/Components/Components/src/PersistentComponentState.cs
similarity index 56%
rename from src/Components/Components/src/Lifetime/ComponentApplicationState.cs
rename to src/Components/Components/src/PersistentComponentState.cs
index 1b25d22c8328..5ab75f106a1a 100644
--- a/src/Components/Components/src/Lifetime/ComponentApplicationState.cs
+++ b/src/Components/Components/src/PersistentComponentState.cs
@@ -1,11 +1,10 @@
// 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 Microsoft.AspNetCore.Components.Infrastructure;
using static Microsoft.AspNetCore.Internal.LinkerFlags;
namespace Microsoft.AspNetCore.Components
@@ -13,70 +12,59 @@ namespace Microsoft.AspNetCore.Components
///
/// The state for the components and services of a components application.
///
- public class ComponentApplicationState
+ public class PersistentComponentState
{
- private IDictionary? _existingState;
- private readonly IDictionary _currentState;
- private readonly List _registeredCallbacks;
+ private IDictionary>? _existingState;
+ private readonly IDictionary _currentState;
- internal ComponentApplicationState(
- IDictionary currentState,
- List pauseCallbacks)
+ private readonly List> _registeredCallbacks;
+
+ internal PersistentComponentState(
+ IDictionary currentState,
+ List> pauseCallbacks)
{
_currentState = currentState;
_registeredCallbacks = pauseCallbacks;
}
- internal void InitializeExistingState(IDictionary existingState)
+ internal bool PersistingState { get; set; }
+
+ internal void InitializeExistingState(IDictionary> existingState)
{
if (_existingState != null)
{
- throw new InvalidOperationException("ComponentApplicationState already initialized.");
+ throw new InvalidOperationException("PersistentComponentState already initialized.");
}
_existingState = existingState ?? throw new ArgumentNullException(nameof(existingState));
}
///
- /// Represents the method that performs operations when is raised and the application is about to be paused.
+ /// 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.
///
- /// A that will complete when the method is done preparing for the application pause.
- public delegate Task OnPersistingCallback();
-
- ///
- /// 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.
- ///
- public event OnPersistingCallback OnPersisting
+ /// The callback to invoke when the application is being paused.
+ /// A subscription that can be used to unregister the callback when disposed.
+ public PersistingComponentStateSubscription RegisterOnPersisting(Func 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);
}
///
/// Tries to retrieve the persisted state with the given .
/// When the key is present, the state is successfully returned via
- /// and removed from the .
+ /// and removed from the .
///
/// The key used to persist the state.
/// The persisted state.
/// true if the state was found; false otherwise.
- public bool TryTakePersistedState(string key, [MaybeNullWhen(false)] out byte[]? value)
+ public bool TryTake(string key, out ReadOnlySequence value)
{
if (key is null)
{
@@ -89,7 +77,7 @@ public bool TryTakePersistedState(string key, [MaybeNullWhen(false)] out byte[]?
// 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;
+ value = default;
return false;
}
@@ -105,27 +93,35 @@ public bool TryTakePersistedState(string key, [MaybeNullWhen(false)] out byte[]?
}
///
- /// Persists the serialized state for the given .
+ /// Persists the serialized state for the given .
///
/// The key to use to persist the state.
- /// The state to persist.
- public void PersistState(string key, byte[] value)
+ /// The state to persist.
+ public void Persist(string key, Action> valueWriter)
{
if (key is null)
{
throw new ArgumentNullException(nameof(key));
}
- if (value is null)
+ if (valueWriter is null)
+ {
+ throw new ArgumentNullException(nameof(valueWriter));
+ }
+
+ if (!PersistingState)
{
- throw new ArgumentNullException(nameof(value));
+ throw new InvalidOperationException("Persisting state is only allowed during an OnPersisting callback.");
}
if (_currentState.ContainsKey(key))
{
throw new ArgumentException($"There is already a persisted object under the same key '{key}'");
}
- _currentState.Add(key, value);
+
+ var writer = new PooledByteBufferWriter();
+ _currentState.Add(key, writer);
+ valueWriter(writer);
}
///
@@ -142,29 +138,47 @@ public void PersistState(string key, byte[] value)
throw new ArgumentNullException(nameof(key));
}
- PersistState(key, JsonSerializer.SerializeToUtf8Bytes(instance, JsonSerializerOptionsProvider.Options));
+ if (key is null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (!PersistingState)
+ {
+ throw new InvalidOperationException("Persisting state is only allowed during an OnPersisting callback.");
+ }
+
+ if (_currentState.ContainsKey(key))
+ {
+ throw new ArgumentException($"There is already a persisted object under the same key '{key}'");
+ }
+
+ var writer = new PooledByteBufferWriter();
+ _currentState.Add(key, writer);
+ JsonSerializer.Serialize(new Utf8JsonWriter(writer), instance, JsonSerializerOptionsProvider.Options);
}
///
/// Tries to retrieve the persisted state as JSON with the given and deserializes it into an
/// instance of type .
/// When the key is present, the state is successfully returned via
- /// and removed from the .
+ /// and removed from the .
///
/// The key used to persist the instance.
/// The persisted instance.
/// true if the state was found; false otherwise.
[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)
+ public bool TryTakeFromJson<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string key, [MaybeNullWhen(false)] out TValue? instance)
{
if (key is null)
{
throw new ArgumentNullException(nameof(key));
}
- if (TryTakePersistedState(key, out var data))
+ if (TryTake(key, out var data))
{
- instance = JsonSerializer.Deserialize(data, JsonSerializerOptionsProvider.Options)!;
+ var reader = new Utf8JsonReader(data);
+ instance = JsonSerializer.Deserialize(ref reader, JsonSerializerOptionsProvider.Options)!;
return true;
}
else
diff --git a/src/Components/Components/src/PersistingComponentStateSubscription.cs b/src/Components/Components/src/PersistingComponentStateSubscription.cs
new file mode 100644
index 000000000000..7ae08a24ed92
--- /dev/null
+++ b/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
+{
+ ///
+ /// Represents a subscription to the OnPersisting callback that callback will trigger
+ /// when the application is being persisted.
+ ///
+ public readonly struct PersistingComponentStateSubscription : IDisposable
+ {
+ private readonly List>? _callbacks;
+ private readonly Func? _callback;
+
+ internal PersistingComponentStateSubscription(List> callbacks, Func callback)
+ {
+ _callbacks = callbacks;
+ _callback = callback;
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (_callback != null)
+ {
+ _callbacks?.Remove(_callback);
+ }
+ }
+ }
+}
diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt
index 9256e9071f44..2d96fbcd4675 100644
--- a/src/Components/Components/src/PublicAPI.Unshipped.txt
+++ b/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(string! key, TValue instance) -> void
-Microsoft.AspNetCore.Components.ComponentApplicationState.PersistState(string! key, byte[]! value) -> void
-Microsoft.AspNetCore.Components.ComponentApplicationState.TryTakeAsJson(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,6 @@ 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! 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!>!
-Microsoft.AspNetCore.Components.Lifetime.IComponentApplicationStateStore.PersistStateAsync(System.Collections.Generic.IReadOnlyDictionary! 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 +28,22 @@ 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.IPersistentComponentStateStore.GetPersistedStateAsync() -> System.Threading.Tasks.Task>!>!
+Microsoft.AspNetCore.Components.IPersistentComponentStateStore.PersistStateAsync(System.Collections.Generic.IReadOnlyDictionary>! state) -> System.Threading.Tasks.Task!
+Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager
+Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.ComponentStatePersistenceManager(Microsoft.Extensions.Logging.ILogger! 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.Persist(string! key, System.Action!>! valueWriter) -> void
+Microsoft.AspNetCore.Components.PersistentComponentState.PersistAsJson(string! key, TValue instance) -> void
+Microsoft.AspNetCore.Components.PersistentComponentState.RegisterOnPersisting(System.Func! callback) -> Microsoft.AspNetCore.Components.PersistingComponentStateSubscription
+Microsoft.AspNetCore.Components.PersistentComponentState.TryTake(string! key, out System.Buffers.ReadOnlySequence value) -> bool
+Microsoft.AspNetCore.Components.PersistentComponentState.TryTakeFromJson(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
diff --git a/src/Components/Components/test/Lifetime/ComponentApplicationLifetimeTest.cs b/src/Components/Components/test/Lifetime/ComponentApplicationLifetimeTest.cs
index d7046bd12a69..8866e3a312be 100644
--- a/src/Components/Components/test/Lifetime/ComponentApplicationLifetimeTest.cs
+++ b/src/Components/Components/test/Lifetime/ComponentApplicationLifetimeTest.cs
@@ -2,9 +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.Linq;
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 +22,19 @@ public class ComponentApplicationLifetimeTest
public async Task RestoreStateAsync_InitializesStateWithDataFromTheProvidedStore()
{
// Arrange
- byte[] data = new byte[] { 0, 1, 2, 3, 4 };
- var state = new Dictionary
+ var data = new ReadOnlySequence(new byte[] { 0, 1, 2, 3, 4 });
+ var state = new Dictionary>
{
["MyState"] = data
};
var store = new TestStore(state);
- var lifetime = new ComponentApplicationLifetime(NullLogger.Instance);
+ var lifetime = new ComponentStatePersistenceManager(NullLogger.Instance);
// Act
await lifetime.RestoreStateAsync(store);
// Assert
- Assert.True(lifetime.State.TryTakePersistedState("MyState", out var retrieved));
+ Assert.True(lifetime.State.TryTake("MyState", out var retrieved));
Assert.Empty(state);
Assert.Equal(data, retrieved);
}
@@ -41,12 +43,12 @@ public async Task RestoreStateAsync_InitializesStateWithDataFromTheProvidedStore
public async Task RestoreStateAsync_ThrowsOnDoubleInitialization()
{
// Arrange
- var state = new Dictionary
+ var state = new Dictionary>
{
- ["MyState"] = new byte[] { 0, 1, 2, 3, 4 }
+ ["MyState"] = new ReadOnlySequence(new byte[] { 0, 1, 2, 3, 4 })
};
var store = new TestStore(state);
- var lifetime = new ComponentApplicationLifetime(NullLogger.Instance);
+ var lifetime = new ComponentStatePersistenceManager(NullLogger.Instance);
await lifetime.RestoreStateAsync(store);
@@ -58,35 +60,39 @@ public async Task RestoreStateAsync_ThrowsOnDoubleInitialization()
public async Task PersistStateAsync_SavesPersistedStateToTheStore()
{
// Arrange
- var state = new Dictionary();
+ var state = new Dictionary>();
var store = new TestStore(state);
- var lifetime = new ComponentApplicationLifetime(NullLogger.Instance);
+ var lifetime = new ComponentStatePersistenceManager(NullLogger.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.Persist("MyState", writer => writer.Write(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, persisted.ToArray());
}
[Fact]
public async Task PersistStateAsync_InvokesPauseCallbacksDuringPersist()
{
// Arrange
- var state = new Dictionary();
+ var state = new Dictionary>();
var store = new TestStore(state);
- var lifetime = new ComponentApplicationLifetime(NullLogger.Instance);
+ var lifetime = new ComponentStatePersistenceManager(NullLogger.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);
@@ -99,9 +105,9 @@ public async Task PersistStateAsync_InvokesPauseCallbacksDuringPersist()
public async Task PersistStateAsync_FiresCallbacksInParallel()
{
// Arrange
- var state = new Dictionary();
+ var state = new Dictionary>();
var store = new TestStore(state);
- var lifetime = new ComponentApplicationLifetime(NullLogger.Instance);
+ var lifetime = new ComponentStatePersistenceManager(NullLogger.Instance);
var renderer = new TestRenderer();
var sequence = new List { };
@@ -109,8 +115,8 @@ public async Task PersistStateAsync_FiresCallbacksInParallel()
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 +129,53 @@ public async Task PersistStateAsync_FiresCallbacksInParallel()
Assert.Equal(new[] { 1, 2, 3, 4 }, sequence);
}
+ [Fact]
+ public async Task PersistStateAsync_CallbacksAreRemovedWhenSubscriptionsAreDisposed()
+ {
+ // Arrange
+ var state = new Dictionary>();
+ var store = new TestStore(state);
+ var lifetime = new ComponentStatePersistenceManager(NullLogger.Instance);
+ var renderer = new TestRenderer();
+
+ var sequence = new List { };
+
+ 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();
- var state = new Dictionary();
+ var logger = loggerFactory.CreateLogger();
+ var state = new Dictionary>();
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 +192,16 @@ public async Task PersistStateAsync_ContinuesInvokingPauseCallbacksDuringPersist
// Arrange
var sink = new TestSink();
var loggerFactory = new TestLoggerFactory(sink, true);
- var logger = loggerFactory.CreateLogger();
- var state = new Dictionary();
+ var logger = loggerFactory.CreateLogger();
+ var state = new Dictionary>();
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);
@@ -182,14 +219,18 @@ public async Task PersistStateAsync_ContinuesInvokingPauseCallbacksDuringPersist
public async Task PersistStateAsync_ThrowsWhenDeveloperTriesToPersistStateMultipleTimes()
{
// Arrange
- var state = new Dictionary();
+ var state = new Dictionary>();
var store = new TestStore(state);
- var lifetime = new ComponentApplicationLifetime(NullLogger.Instance);
+ var lifetime = new ComponentStatePersistenceManager(NullLogger.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.Persist("MyState", writer => writer.Write(new byte[] { 1, 2, 3, 4 }));
+ return Task.CompletedTask;
+ });
// Act
await lifetime.PersistStateAsync(store, renderer);
@@ -219,23 +260,24 @@ protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
}
}
- private class TestStore : IComponentApplicationStateStore
+ private class TestStore : IPersistentComponentStateStore
{
- public TestStore(IDictionary initialState)
+ public TestStore(IDictionary> initialState)
{
State = initialState;
}
- public IDictionary State { get; set; }
+ public IDictionary> State { get; set; }
- public Task> GetPersistedStateAsync()
+ public Task>> GetPersistedStateAsync()
{
return Task.FromResult(State);
}
- public Task PersistStateAsync(IReadOnlyDictionary state)
+ public Task PersistStateAsync(IReadOnlyDictionary> state)
{
- State = new Dictionary(state);
+ // We copy the data here because it's no longer available after this call completes.
+ State = state.ToDictionary(kvp => kvp.Key, kvp => new ReadOnlySequence(kvp.Value.ToArray()));
return Task.CompletedTask;
}
}
diff --git a/src/Components/Components/test/Lifetime/ComponentApplicationStateTest.cs b/src/Components/Components/test/Lifetime/ComponentApplicationStateTest.cs
index d9a434260efe..53e4a8e764e1 100644
--- a/src/Components/Components/test/Lifetime/ComponentApplicationStateTest.cs
+++ b/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,28 +19,28 @@ public class ComponentApplicationStateTest
public void InitializeExistingState_SetupsState()
{
// Arrange
- var applicationState = new ComponentApplicationState(new Dictionary(), new List());
- var existingState = new Dictionary
+ var applicationState = new PersistentComponentState(new Dictionary(), new List>());
+ var existingState = new Dictionary>
{
- ["MyState"] = new byte[] { 1, 2, 3, 4 }
+ ["MyState"] = new ReadOnlySequence(new byte[] { 1, 2, 3, 4 })
};
// Act
applicationState.InitializeExistingState(existingState);
// Assert
- Assert.True(applicationState.TryTakePersistedState("MyState", out var existing));
- Assert.Equal(new byte[] { 1, 2, 3, 4 }, existing);
+ Assert.True(applicationState.TryTake("MyState", out var existing));
+ Assert.Equal(new byte[] { 1, 2, 3, 4 }, existing.ToArray());
}
[Fact]
public void InitializeExistingState_ThrowsIfAlreadyInitialized()
{
// Arrange
- var applicationState = new ComponentApplicationState(new Dictionary(), new List());
- var existingState = new Dictionary
+ var applicationState = new PersistentComponentState(new Dictionary(), new List>());
+ var existingState = new Dictionary>
{
- ["MyState"] = new byte[] { 1, 2, 3, 4 }
+ ["MyState"] = new ReadOnlySequence(new byte[] { 1, 2, 3, 4 })
};
applicationState.InitializeExistingState(existingState);
@@ -49,57 +53,60 @@ public void InitializeExistingState_ThrowsIfAlreadyInitialized()
public void TryRetrieveState_ReturnsStateWhenItExists()
{
// Arrange
- var applicationState = new ComponentApplicationState(new Dictionary(), new List());
- var existingState = new Dictionary
+ var applicationState = new PersistentComponentState(new Dictionary(), new List>());
+ var existingState = new Dictionary>
{
- ["MyState"] = new byte[] { 1, 2, 3, 4 }
+ ["MyState"] = new ReadOnlySequence(new byte[] { 1, 2, 3, 4 })
};
// Act
applicationState.InitializeExistingState(existingState);
// Assert
- Assert.True(applicationState.TryTakePersistedState("MyState", out var existing));
- Assert.Equal(new byte[] { 1, 2, 3, 4 }, existing);
- Assert.False(applicationState.TryTakePersistedState("MyState", out var gone));
+ Assert.True(applicationState.TryTake("MyState", out var existing));
+ Assert.Equal(new byte[] { 1, 2, 3, 4 }, existing.ToArray());
+ Assert.False(applicationState.TryTake("MyState", out var gone));
}
[Fact]
- public void PersistState_SavesDataToTheStore()
+ public void PersistState_SavesDataToTheStoreAsync()
{
// Arrange
- var currentState = new Dictionary();
- var applicationState = new ComponentApplicationState(currentState, new List());
+ var currentState = new Dictionary();
+ var applicationState = new PersistentComponentState(currentState, new List>());
+ applicationState.PersistingState = true;
var myState = new byte[] { 1, 2, 3, 4 };
// Act
- applicationState.PersistState("MyState", myState);
+ applicationState.Persist("MyState", writer => writer.Write(myState));
// Assert
Assert.True(currentState.TryGetValue("MyState", out var stored));
- Assert.Equal(myState, stored);
+ Assert.Equal(myState, stored.WrittenMemory.Span.ToArray());
}
[Fact]
public void PersistState_ThrowsForDuplicateKeys()
{
// Arrange
- var currentState = new Dictionary();
- var applicationState = new ComponentApplicationState(currentState, new List());
+ var currentState = new Dictionary();
+ var applicationState = new PersistentComponentState(currentState, new List>());
+ applicationState.PersistingState = true;
var myState = new byte[] { 1, 2, 3, 4 };
- applicationState.PersistState("MyState", myState);
+ applicationState.Persist("MyState", writer => writer.Write(myState));
// Act & Assert
- Assert.Throws(() => applicationState.PersistState("MyState", myState));
+ Assert.Throws(() => applicationState.Persist("MyState", writer => writer.Write(myState)));
}
[Fact]
- public void PersistAsJson_SerializesTheDataToJson()
+ public void PersistAsJson_SerializesTheDataToJsonAsync()
{
// Arrange
- var currentState = new Dictionary();
- var applicationState = new ComponentApplicationState(currentState, new List());
+ var currentState = new Dictionary();
+ var applicationState = new PersistentComponentState(currentState, new List>());
+ applicationState.PersistingState = true;
var myState = new byte[] { 1, 2, 3, 4 };
// Act
@@ -107,22 +114,23 @@ public void PersistAsJson_SerializesTheDataToJson()
// Assert
Assert.True(currentState.TryGetValue("MyState", out var stored));
- Assert.Equal(myState, JsonSerializer.Deserialize(stored));
+ Assert.Equal(myState, JsonSerializer.Deserialize(stored.WrittenMemory.Span));
}
[Fact]
- public void PersistAsJson_NullValue()
+ public void PersistAsJson_NullValueAsync()
{
// Arrange
- var currentState = new Dictionary();
- var applicationState = new ComponentApplicationState(currentState, new List());
+ var currentState = new Dictionary();
+ var applicationState = new PersistentComponentState(currentState, new List>());
+ applicationState.PersistingState = true;
// Act
applicationState.PersistAsJson("MyState", null);
// Assert
Assert.True(currentState.TryGetValue("MyState", out var stored));
- Assert.Null(JsonSerializer.Deserialize(stored));
+ Assert.Null(JsonSerializer.Deserialize(stored.WrittenMemory.Span));
}
[Fact]
@@ -131,17 +139,17 @@ public void TryRetrieveFromJson_DeserializesTheDataFromJson()
// Arrange
var myState = new byte[] { 1, 2, 3, 4 };
var serialized = JsonSerializer.SerializeToUtf8Bytes(myState);
- var existingState = new Dictionary() { ["MyState"] = serialized };
- var applicationState = new ComponentApplicationState(new Dictionary(), new List());
+ var existingState = new Dictionary>() { ["MyState"] = new ReadOnlySequence(serialized) };
+ var applicationState = new PersistentComponentState(new Dictionary(), new List>());
applicationState.InitializeExistingState(existingState);
// Act
- Assert.True(applicationState.TryTakeAsJson("MyState", out var stored));
+ Assert.True(applicationState.TryTakeFromJson("MyState", out var stored));
// Assert
Assert.Equal(myState, stored);
- Assert.False(applicationState.TryTakeAsJson("MyState", out _));
+ Assert.False(applicationState.TryTakeFromJson("MyState", out _));
}
[Fact]
@@ -149,17 +157,17 @@ public void TryRetrieveFromJson_NullValue()
{
// Arrange
var serialized = JsonSerializer.SerializeToUtf8Bytes(null);
- var existingState = new Dictionary() { ["MyState"] = serialized };
- var applicationState = new ComponentApplicationState(new Dictionary(), new List());
+ var existingState = new Dictionary>() { ["MyState"] = new ReadOnlySequence(serialized) };
+ var applicationState = new PersistentComponentState(new Dictionary(), new List>());
applicationState.InitializeExistingState(existingState);
// Act
- Assert.True(applicationState.TryTakeAsJson("MyState", out var stored));
+ Assert.True(applicationState.TryTakeFromJson("MyState", out var stored));
// Assert
Assert.Null(stored);
- Assert.False(applicationState.TryTakeAsJson("MyState", out _));
+ Assert.False(applicationState.TryTakeFromJson("MyState", out _));
}
}
}
diff --git a/src/Components/Server/src/Circuits/CircuitFactory.cs b/src/Components/Server/src/Circuits/CircuitFactory.cs
index aa5a3e20a1f7..fbc29aadbeb5 100644
--- a/src/Components/Server/src/Circuits/CircuitFactory.cs
+++ b/src/Components/Server/src/Circuits/CircuitFactory.cs
@@ -6,7 +6,7 @@
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 @@ public async ValueTask CreateCircuitHostAsync(
string baseUri,
string uri,
ClaimsPrincipal user,
- IComponentApplicationStateStore store)
+ IPersistentComponentStateStore store)
{
var scope = _scopeFactory.CreateAsyncScope();
var jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService();
@@ -63,7 +63,7 @@ public async ValueTask CreateCircuitHostAsync(
navigationManager.Initialize(baseUri, uri);
}
- var appLifetime = scope.ServiceProvider.GetRequiredService();
+ var appLifetime = scope.ServiceProvider.GetRequiredService();
await appLifetime.RestoreStateAsync(store);
var jsComponentInterop = new CircuitJSComponentInterop(_options);
diff --git a/src/Components/Server/src/Circuits/CircuitHandleRegistry.cs b/src/Components/Server/src/Circuits/CircuitHandleRegistry.cs
index 3591eda47861..5414e9a031f6 100644
--- a/src/Components/Server/src/Circuits/CircuitHandleRegistry.cs
+++ b/src/Components/Server/src/Circuits/CircuitHandleRegistry.cs
@@ -6,7 +6,6 @@
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
diff --git a/src/Components/Server/test/Circuits/ComponentHubTest.cs b/src/Components/Server/test/Circuits/ComponentHubTest.cs
index 210b4eeeef89..9fd79f65126d 100644
--- a/src/Components/Server/test/Circuits/ComponentHubTest.cs
+++ b/src/Components/Server/test/Circuits/ComponentHubTest.cs
@@ -7,7 +7,7 @@
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;
@@ -181,7 +181,7 @@ public ValueTask CreateCircuitHostAsync(
string baseUri,
string uri,
ClaimsPrincipal user,
- IComponentApplicationStateStore store)
+ IPersistentComponentStateStore store)
{
var serviceScope = new Mock();
var circuitHost = TestCircuitHost.Create(serviceScope: new AsyncServiceScope(serviceScope.Object));
diff --git a/src/Components/WebAssembly/Samples/HostedBlazorWebassemblyApp/Client/Pages/FetchData.razor b/src/Components/WebAssembly/Samples/HostedBlazorWebassemblyApp/Client/Pages/FetchData.razor
index 1e0b4f51c4c1..5450f197c2ab 100644
--- a/src/Components/WebAssembly/Samples/HostedBlazorWebassemblyApp/Client/Pages/FetchData.razor
+++ b/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
Weather forecast
@@ -39,13 +39,19 @@ else
@code {
private WeatherForecast[] forecasts = Array.Empty();
+ private PersistingComponentStateSubscription _persistingSubscription;
protected override async Task OnInitializedAsync()
{
- ApplicationState.OnPersisting += PersistForecasts;
- forecasts = !ApplicationState.TryTakeAsJson("fetchdata", out var restored) ?
- await WeatherForecastService.GetForecastAsync(DateTime.Now) :
- restored!;
+ _persistingSubscription = ApplicationState.RegisterOnPersisting(PersistForecasts);
+ if(!ApplicationState.TryTakeFromJson("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();
}
}
diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs
index f38ba877a1e7..42522b5a11d3 100644
--- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs
+++ b/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 @@ internal async Task RunAsyncCore(CancellationToken cancellationToken, WebAssembl
// This is the earliest opportunity to fetch satellite assemblies for this selection.
await cultureProvider.LoadCurrentCultureResourcesAsync();
- var manager = Services.GetRequiredService();
+ var manager = Services.GetRequiredService();
var store = !string.IsNullOrEmpty(_persistedState) ?
new PrerenderComponentApplicationStore(_persistedState) :
new PrerenderComponentApplicationStore();
diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs
index 8176d3635b92..53376f14c1f1 100644
--- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs
+++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs
@@ -6,7 +6,8 @@
using System.Globalization;
using System.IO;
using System.Text.Json;
-using Microsoft.AspNetCore.Components.Lifetime;
+using System.Linq;
+using Microsoft.AspNetCore.Components.Infrastructure;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
@@ -254,8 +255,8 @@ internal void InitializeDefaultServices()
Services.AddSingleton(WebAssemblyNavigationManager.Instance);
Services.AddSingleton(WebAssemblyNavigationInterception.Instance);
Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance));
- Services.AddSingleton();
- Services.AddSingleton(sp => sp.GetRequiredService().State);
+ Services.AddSingleton();
+ Services.AddSingleton(sp => sp.GetRequiredService().State);
Services.AddSingleton();
Services.AddLogging(builder =>
{
diff --git a/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj b/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj
index edd1d5ef6c81..69a68e966989 100644
--- a/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj
+++ b/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj
@@ -33,6 +33,7 @@
+
diff --git a/src/Components/test/E2ETest/Tests/CircuitTests.cs b/src/Components/test/E2ETest/Tests/CircuitTests.cs
index 39ab552af5a5..e4354bb1d4f0 100644
--- a/src/Components/test/E2ETest/Tests/CircuitTests.cs
+++ b/src/Components/test/E2ETest/Tests/CircuitTests.cs
@@ -12,7 +12,7 @@
using OpenQA.Selenium;
using TestServer;
using Xunit;
-using Microsoft.AspNetCore.Components.Lifetime;
+using Microsoft.AspNetCore.Components.Infrastructure;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
@@ -99,4 +99,4 @@ void AssertLogContains(params string[] messages)
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Components/test/testassets/BasicTestApp/PreserveStateComponent.razor b/src/Components/test/testassets/BasicTestApp/PreserveStateComponent.razor
index 62eb0acb5fe9..f8254259f626 100644
--- a/src/Components/test/testassets/BasicTestApp/PreserveStateComponent.razor
+++ b/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
@State: @_state
@@ -40,30 +42,47 @@
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.TryTake(State, out var preserved))
{
_state = Encoding.UTF8.GetString(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.Persist(State, writer => WriteUtf8Bytes(writer, _state));
+ if(ExtraState != null)
+ {
+ AppState.Persist(ExtraState, writer => WriteUtf8Bytes(writer, _extraState));
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+
+ static void WriteUtf8Bytes(IBufferWriter writer, string state)
+ {
+ var bytes = Encoding.UTF8.GetBytes(state);
+ writer.Write(bytes);
}
}
public void DisplayExtraState()
{
- if (AppState.TryTakePersistedState(ExtraState, out var extraState))
+ if (AppState.TryTake(ExtraState, out var extraState))
{
_extraStateAvailable = true;
_extraState = Encoding.UTF8.GetString(extraState);
@@ -74,4 +93,9 @@
_extraState = Guid.NewGuid().ToString();
}
}
+
+ void IDisposable.Dispose()
+ {
+ _registration.Dispose();
+ }
}
diff --git a/src/Components/test/testassets/BasicTestApp/PreserveStateService.cs b/src/Components/test/testassets/BasicTestApp/PreserveStateService.cs
index 12b983faaeed..e511a121152e 100644
--- a/src/Components/test/testassets/BasicTestApp/PreserveStateService.cs
+++ b/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 @@ public PreserveStateService(ComponentApplicationState componentApplicationState)
private void TryRestoreState()
{
- if (_componentApplicationState.TryTakeAsJson("Service", out var state))
+ if (_componentApplicationState.TryTakeFromJson("Service", out var state))
{
_state = state;
}
@@ -42,10 +43,7 @@ private Task PersistState()
return Task.CompletedTask;
}
- public void Dispose()
- {
- _componentApplicationState.OnPersisting -= PersistState;
- }
+ public void Dispose() => _persistingSubscription.Dispose();
private class ServiceState
{
diff --git a/src/Components/test/testassets/TestServer/Properties/launchSettings.json b/src/Components/test/testassets/TestServer/Properties/launchSettings.json
index 363c51bfb4e2..cc8a3eab198e 100644
--- a/src/Components/test/testassets/TestServer/Properties/launchSettings.json
+++ b/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",
diff --git a/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs
index 0ccd9da96f8b..a85cb99630fa 100644
--- a/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs
+++ b/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 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu
}
var services = ViewContext.HttpContext.RequestServices;
- var manager = services.GetRequiredService();
+ var manager = services.GetRequiredService();
var renderer = services.GetRequiredService();
var store = PersistenceMode switch
{
@@ -88,9 +90,29 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu
output.Content.SetHtmlContent(
new HtmlContentBuilder()
.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.Span);
+ _store.Dispose();
+ _store = null;
+ }
+ }
+ }
}
}
diff --git a/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs
index 40ab053eace5..5de4b1d5fbea 100644
--- a/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs
+++ b/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs
@@ -5,8 +5,7 @@
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 @@ private ViewContext GetViewContext()
{
RequestServices = new ServiceCollection()
.AddSingleton(renderer)
- .AddSingleton(new ComponentApplicationLifetime(NullLogger.Instance))
+ .AddSingleton(new ComponentStatePersistenceManager(NullLogger.Instance))
.AddSingleton()
.AddSingleton(_ephemeralProvider)
.AddSingleton(NullLoggerFactory.Instance)
diff --git a/src/Mvc/Mvc.TagHelpers/test/PrerenderComponentApplicationStoreTest.cs b/src/Mvc/Mvc.TagHelpers/test/PrerenderComponentApplicationStoreTest.cs
index e6002fe27b43..b78f34145aee 100644
--- a/src/Mvc/Mvc.TagHelpers/test/PrerenderComponentApplicationStoreTest.cs
+++ b/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;
@@ -17,16 +19,16 @@ public async Task PersistStateAsync_PersistsGivenState()
// Arrange
var expected = "eyJNeVZhbHVlIjoiQVFJREJBPT0ifQ==";
var store = new PrerenderComponentApplicationStore();
- var state = new Dictionary()
+ var state = new Dictionary>()
{
- ["MyValue"] = new byte[] {1,2,3,4}
+ ["MyValue"] = new ReadOnlySequence(new byte[] {1,2,3,4})
};
// Act
await store.PersistStateAsync(state);
// Assert
- Assert.Equal(expected, store.PersistedState);
+ Assert.Equal(expected, store.PersistedState.Span.ToString());
}
[Fact]
@@ -35,16 +37,18 @@ public async Task GetPersistedStateAsync_RestoresPreexistingStateAsync()
// Arrange
var persistedState = "eyJNeVZhbHVlIjoiQVFJREJBPT0ifQ==";
var store = new PrerenderComponentApplicationStore(persistedState);
- var expected = new Dictionary()
+ var expected = new Dictionary>()
{
- ["MyValue"] = new byte[] { 1, 2, 3, 4 }
+ ["MyValue"] = new ReadOnlySequence(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()));
}
}
}
diff --git a/src/Mvc/Mvc.TagHelpers/test/ProtectedPrerenderComponentApplicationStateTest.cs b/src/Mvc/Mvc.TagHelpers/test/ProtectedPrerenderComponentApplicationStateTest.cs
index 83c0f7f2a5ae..854950be23ec 100644
--- a/src/Mvc/Mvc.TagHelpers/test/ProtectedPrerenderComponentApplicationStateTest.cs
+++ b/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;
@@ -24,23 +26,23 @@ public async Task PersistStateAsync_ProtectsPersistedState()
var expected = @"{""MyValue"":""AQIDBA==""}";
var store = new ProtectedPrerenderComponentApplicationStore(_provider);
- var state = new Dictionary()
+ var state = new Dictionary>()
{
- ["MyValue"] = new byte[] { 1, 2, 3, 4 }
+ ["MyValue"] = new ReadOnlySequence(new byte[] { 1, 2, 3, 4 })
};
// Act
await store.PersistStateAsync(state);
// Assert
- Assert.Equal(expected, _protector.Unprotect(store.PersistedState));
+ Assert.Equal(expected, _protector.Unprotect(store.PersistedState.Span.ToString()));
}
[Fact]
public async Task GetPersistStateAsync_CanUnprotectPersistedState()
{
// Arrange
- var expectedState = new Dictionary()
+ var expectedState = new Dictionary()
{
["MyValue"] = new byte[] { 1, 2, 3, 4 }
};
@@ -52,7 +54,9 @@ public async Task GetPersistStateAsync_CanUnprotectPersistedState()
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]
diff --git a/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs b/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs
index d23fee1a94e1..5c5300f125b9 100644
--- a/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs
+++ b/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs
@@ -5,7 +5,7 @@
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 @@ internal static void AddViewServices(IServiceCollection services)
services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
- services.TryAddScoped();
- services.TryAddScoped(sp => sp.GetRequiredService().State);
+ services.TryAddScoped();
+ services.TryAddScoped(sp => sp.GetRequiredService().State);
services.TryAddScoped();
services.TryAddTransient();
diff --git a/src/Mvc/Mvc.ViewFeatures/src/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj b/src/Mvc/Mvc.ViewFeatures/src/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj
index f8674f725c6c..e14055dd922a 100644
--- a/src/Mvc/Mvc.ViewFeatures/src/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj
+++ b/src/Mvc/Mvc.ViewFeatures/src/Microsoft.AspNetCore.Mvc.ViewFeatures.csproj
@@ -40,6 +40,7 @@
+
diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs
index bc01433b7cdf..6b8c9539fe77 100644
--- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs
+++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs
@@ -6,7 +6,7 @@
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 @@ async Task InitializeCore(HttpContext httpContext)
// 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();
+ var componentApplicationLifetime = httpContext.RequestServices.GetRequiredService();
await componentApplicationLifetime.RestoreStateAsync(new PrerenderComponentApplicationStore());
}
}
diff --git a/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentRendererTest.cs b/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentRendererTest.cs
index a2b10b358a90..3b15c2f7def1 100644
--- a/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentRendererTest.cs
+++ b/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentRendererTest.cs
@@ -9,7 +9,7 @@
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 @@ private static ServiceCollection CreateDefaultServiceCollection()
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton, NullLogger>();
- services.AddSingleton();
- services.AddSingleton(sp => sp.GetRequiredService().State);
+ services.AddSingleton, NullLogger>();
+ services.AddSingleton();
+ services.AddSingleton(sp => sp.GetRequiredService().State);
return services;
}
diff --git a/src/Mvc/perf/Microbenchmarks/Microsoft.AspNetCore.Mvc/PreserveComponentStateBenchmark.cs b/src/Mvc/perf/Microbenchmarks/Microsoft.AspNetCore.Mvc/PreserveComponentStateBenchmark.cs
new file mode 100644
index 000000000000..7f485bd8a4cc
--- /dev/null
+++ b/src/Mvc/perf/Microbenchmarks/Microsoft.AspNetCore.Mvc/PreserveComponentStateBenchmark.cs
@@ -0,0 +1,107 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+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> _childContent =
+ (_, __) => Task.FromResult(new DefaultTagHelperContent() as TagHelperContent);
+ private IServiceProvider _serviceProvider;
+ private IServiceScope _serviceScope;
+ private TagHelperOutput _output;
+ private Dictionary _entries = new();
+
+ private byte[] _entryValue;
+
+ public PreserveComponentStateBenchmark()
+ {
+ _context = new TagHelperContext(_attributes, new Dictionary(), "asdf");
+ _serviceProvider = new ServiceCollection()
+ .AddSingleton(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();
+ foreach (var (key,value) in _entries)
+ {
+ state.Persist(key, writer => writer.Write(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().CreateScope();
+ var httpContext = new DefaultHttpContext
+ {
+ RequestServices = _serviceScope.ServiceProvider
+ };
+
+ return new ViewContext
+ {
+ HttpContext = httpContext,
+ };
+ }
+ }
+}
diff --git a/src/Shared/Components/PooledByteBufferWritter.cs b/src/Shared/Components/PooledByteBufferWritter.cs
new file mode 100644
index 000000000000..e931d2c1b1ba
--- /dev/null
+++ b/src/Shared/Components/PooledByteBufferWritter.cs
@@ -0,0 +1,116 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Buffers;
+
+namespace Microsoft.AspNetCore.Components.Infrastructure
+{
+ internal sealed class PooledByteBufferWriter : IBufferWriter, IDisposable
+ {
+ private byte[] _currentBuffer;
+ private int _index;
+ private readonly bool _owned;
+
+ private const int MinimumBufferSize = 256;
+
+ public PooledByteBufferWriter(int initialCapacity = 0)
+ {
+ _currentBuffer = ArrayPool.Shared.Rent(initialCapacity);
+ _index = 0;
+ }
+
+ public PooledByteBufferWriter(byte[] existingBuffer)
+ {
+ _currentBuffer = existingBuffer;
+ _index = existingBuffer.Length;
+ _owned = true;
+ }
+
+ public ReadOnlyMemory WrittenMemory => _currentBuffer.AsMemory(0, _index);
+
+ public int WrittenCount => _index;
+
+ public int Capacity => _currentBuffer.Length;
+
+ public int FreeCapacity => _currentBuffer.Length - _index;
+
+ public void Clear() => ClearHelper();
+
+ private void ClearHelper()
+ {
+ _currentBuffer.AsSpan(0, _index).Clear();
+ _index = 0;
+ }
+
+ // Returns the rented buffer back to the pool
+ public void Dispose()
+ {
+ if (_currentBuffer == null || _owned)
+ {
+ return;
+ }
+
+ ClearHelper();
+ var currentBuffer = _currentBuffer;
+ _currentBuffer = null!;
+ ArrayPool.Shared.Return(currentBuffer);
+ }
+
+ public void Advance(int count)
+ {
+ _index += count;
+ }
+
+ public Memory GetMemory(int sizeHint = 0)
+ {
+ CheckAndResizeBuffer(sizeHint);
+ return _currentBuffer.AsMemory(_index);
+ }
+
+ public Span GetSpan(int sizeHint = 0)
+ {
+ CheckAndResizeBuffer(sizeHint);
+ return _currentBuffer.AsSpan(_index);
+ }
+
+ private void CheckAndResizeBuffer(int sizeHint)
+ {
+ if (_owned)
+ {
+ throw new InvalidOperationException("Can't grow a buffer created from a given array.");
+ }
+
+ if (sizeHint == 0)
+ {
+ sizeHint = MinimumBufferSize;
+ }
+
+ var availableSpace = _currentBuffer.Length - _index;
+
+ if (sizeHint > availableSpace)
+ {
+ var currentLength = _currentBuffer.Length;
+ var growBy = Math.Max(sizeHint, currentLength);
+
+ var newSize = currentLength + growBy;
+
+ if ((uint)newSize > int.MaxValue)
+ {
+ newSize = currentLength + sizeHint;
+ if ((uint)newSize > int.MaxValue)
+ {
+ throw new OutOfMemoryException();
+ }
+ }
+
+ var oldBuffer = _currentBuffer;
+
+ _currentBuffer = ArrayPool.Shared.Rent(newSize);
+
+ var previousBuffer = oldBuffer.AsSpan(0, _index);
+ previousBuffer.CopyTo(_currentBuffer);
+ ArrayPool.Shared.Return(oldBuffer, clearArray: true);
+ }
+ }
+ }
+}
diff --git a/src/Shared/Components/PrerenderComponentApplicationStore.cs b/src/Shared/Components/PrerenderComponentApplicationStore.cs
index df0de6d9bd47..ba6763890451 100644
--- a/src/Shared/Components/PrerenderComponentApplicationStore.cs
+++ b/src/Shared/Components/PrerenderComponentApplicationStore.cs
@@ -1,17 +1,20 @@
// 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.Buffers.Text;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components.Lifetime;
+using Microsoft.AspNetCore.Components.Infrastructure;
namespace Microsoft.AspNetCore.Components
{
- internal class PrerenderComponentApplicationStore : IComponentApplicationStateStore
+ internal class PrerenderComponentApplicationStore : IPersistentComponentStateStore
{
+#nullable enable
+ private char[]? _buffer;
+#nullable disable
+
public PrerenderComponentApplicationStore()
{
ExistingState = new();
@@ -25,34 +28,113 @@ public PrerenderComponentApplicationStore(string existingState)
throw new ArgumentNullException(nameof(existingState));
}
- ExistingState = JsonSerializer.Deserialize>(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>(existingState);
+ if (state == null)
+ {
+ throw new ArgumentException("Could not deserialize state correctly", nameof(existingState));
+ }
+
+ var stateDictionary = new Dictionary>();
+ foreach (var (key, value) in state)
+ {
+ stateDictionary.Add(key, new ReadOnlySequence(value));
+ }
+
+ ExistingState = stateDictionary;
}
#nullable enable
- public string? PersistedState { get; private set; }
+ public ReadOnlyMemory PersistedState { get; private set; }
#nullable disable
- public Dictionary ExistingState { get; protected set; }
+ public Dictionary> ExistingState { get; protected set; }
- public Task> GetPersistedStateAsync()
+ public Task>> GetPersistedStateAsync()
{
- return Task.FromResult((IDictionary)ExistingState);
+ return Task.FromResult((IDictionary>)ExistingState);
}
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Simple serialize of primitive types.")]
- protected virtual byte[] SerializeState(IReadOnlyDictionary state)
+ protected virtual PooledByteBufferWriter SerializeState(IReadOnlyDictionary> state)
{
- return JsonSerializer.SerializeToUtf8Bytes(state);
+ // System.Text.Json doesn't support serializing ReadonlySequence so we need to buffer
+ // the data with a memory pool here. We will change our serialization strategy in the future here
+ // so that we can avoid this step.
+ var buffer = new PooledByteBufferWriter();
+ try
+ {
+ var jsonWriter = new Utf8JsonWriter(buffer);
+ jsonWriter.WriteStartObject();
+ foreach (var (key, value) in state)
+ {
+ if (value.IsSingleSegment)
+ {
+ jsonWriter.WriteBase64String(key, value.First.Span);
+ }
+ else
+ {
+ WriteMultipleSegments(jsonWriter, key, value);
+ }
+ jsonWriter.Flush();
+ }
+
+ jsonWriter.WriteEndObject();
+ jsonWriter.Flush();
+ return buffer;
+
+ }
+ catch
+ {
+ buffer.Dispose();
+ throw;
+ }
+ static void WriteMultipleSegments(Utf8JsonWriter jsonWriter, string key, ReadOnlySequence value)
+ {
+ byte[] unescapedArray = null;
+ var valueLength = (int)value.Length;
+
+ Span valueSegment = value.Length <= 256 ?
+ stackalloc byte[valueLength] :
+ (unescapedArray = ArrayPool.Shared.Rent(valueLength)).AsSpan().Slice(0, valueLength);
+
+ value.CopyTo(valueSegment);
+ jsonWriter.WriteBase64String(key, valueSegment);
+
+ if (unescapedArray != null)
+ {
+ valueSegment.Clear();
+ ArrayPool.Shared.Return(unescapedArray);
+ }
+ }
}
- public Task PersistStateAsync(IReadOnlyDictionary state)
+ public Task PersistStateAsync(IReadOnlyDictionary> state)
{
- var bytes = SerializeState(state);
+ using var bytes = SerializeState(state);
+ var length = Base64.GetMaxEncodedToUtf8Length(bytes.WrittenCount);
+ // We can do this because the representation in bytes for characters in the base64 alphabet for utf-8 is 1.
+ _buffer = ArrayPool.Shared.Rent(length);
+
+ var memory = _buffer.AsMemory().Slice(0, length);
+ Convert.TryToBase64Chars(bytes.WrittenMemory.Span, memory.Span, out _);
+ PersistedState = memory;
- var result = Convert.ToBase64String(bytes);
- PersistedState = result;
return Task.CompletedTask;
}
+
+ public void Dispose()
+ {
+ if (_buffer != null)
+ {
+ ArrayPool.Shared.Return(_buffer, true);
+ _buffer = null;
+ }
+ }
}
}
diff --git a/src/Shared/Components/ProtectedPrerenderComponentApplicationStore.cs b/src/Shared/Components/ProtectedPrerenderComponentApplicationStore.cs
index 7af96de683f4..50e7ecc8b0f1 100644
--- a/src/Shared/Components/ProtectedPrerenderComponentApplicationStore.cs
+++ b/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,15 +23,17 @@ public ProtectedPrerenderComponentApplicationStore(IDataProtectionProvider dataP
public ProtectedPrerenderComponentApplicationStore(string existingState, IDataProtectionProvider dataProtectionProvider)
{
CreateProtector(dataProtectionProvider);
- ExistingState = JsonSerializer.Deserialize>(_protector.Unprotect(Convert.FromBase64String(existingState)));
+ DeserializeState(_protector.Unprotect(Convert.FromBase64String(existingState)));
}
- protected override byte[] SerializeState(IReadOnlyDictionary state)
+ protected override PooledByteBufferWriter SerializeState(IReadOnlyDictionary> state)
{
var bytes = base.SerializeState(state);
if (_protector != null)
{
- bytes = _protector.Protect(bytes);
+ var newBuffer = new PooledByteBufferWriter(_protector.Protect(bytes.WrittenMemory.Span.ToArray()));
+ bytes.Dispose();
+ return newBuffer;
}
return bytes;
diff --git a/src/submodules/googletest b/src/submodules/googletest
index ff21b36e1e85..2f80c2ba71c0 160000
--- a/src/submodules/googletest
+++ b/src/submodules/googletest
@@ -1 +1 @@
-Subproject commit ff21b36e1e858bf9274ed2bbf4921e415363c393
+Subproject commit 2f80c2ba71c0e8922a03b9b855e5b019ad1f7064