Skip to content

Commit dcf7a6a

Browse files
committed
Apply API review feedback
1 parent 52de4ad commit dcf7a6a

26 files changed

+552
-144
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Collections.Generic;
5-
using System.Threading.Tasks;
4+
using System.Buffers;
65

7-
namespace Microsoft.AspNetCore.Components.Lifetime
6+
namespace Microsoft.AspNetCore.Components
87
{
98
/// <summary>
109
/// Manages the storage for components and services that are part of a Blazor application.
1110
/// </summary>
12-
public interface IComponentApplicationStateStore
11+
public interface IPersistentComponentStateStore
1312
{
1413
/// <summary>
1514
/// Gets the persisted state from the store.
1615
/// </summary>
1716
/// <returns>The persisted state.</returns>
18-
Task<IDictionary<string, byte[]>> GetPersistedStateAsync();
17+
Task<IDictionary<string, ReadOnlySequence<byte>>> GetPersistedStateAsync();
1918

2019
/// <summary>
2120
/// Persists the serialized state into the storage.
2221
/// </summary>
2322
/// <param name="state">The serialized state to persist.</param>
2423
/// <returns>A <see cref="Task" /> that completes when the state is persisted to disk.</returns>
25-
Task PersistStateAsync(IReadOnlyDictionary<string, byte[]> state);
24+
Task PersistStateAsync(IReadOnlyDictionary<string, ReadOnlySequence<byte>> state);
2625
}
2726
}

src/Components/Components/src/Lifetime/ComponentApplicationLifetime.cs renamed to src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,60 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Buffers;
56
using System.Collections.Generic;
67
using System.Collections.ObjectModel;
8+
using System.IO.Pipelines;
9+
using System.Text.Json;
10+
using System.Text.Json.Serialization;
711
using System.Threading.Tasks;
812
using Microsoft.AspNetCore.Components.RenderTree;
913
using Microsoft.Extensions.Logging;
1014

11-
namespace Microsoft.AspNetCore.Components.Lifetime
15+
namespace Microsoft.AspNetCore.Components.Infrastructure
1216
{
1317
/// <summary>
14-
/// Manages the lifetime of a component application.
18+
/// Manages the persistent state of components in an application.
1519
/// </summary>
16-
public class ComponentApplicationLifetime
20+
public class ComponentStatePersistenceManager : IDisposable
1721
{
1822
private bool _stateIsPersisted;
19-
private readonly List<ComponentApplicationState.OnPersistingCallback> _pauseCallbacks = new();
20-
private readonly Dictionary<string, byte[]> _currentState = new();
21-
private readonly ILogger<ComponentApplicationLifetime> _logger;
23+
private readonly List<Func<Task>> _pauseCallbacks = new();
24+
private readonly Dictionary<string, PooledByteBufferWriter> _currentState = new(StringComparer.Ordinal);
25+
private readonly ILogger<ComponentStatePersistenceManager> _logger;
2226

2327
/// <summary>
24-
/// Initializes a new instance of <see cref="ComponentApplicationLifetime"/>.
28+
/// Initializes a new instance of <see cref="ComponentStatePersistenceManager"/>.
2529
/// </summary>
26-
public ComponentApplicationLifetime(ILogger<ComponentApplicationLifetime> logger)
30+
public ComponentStatePersistenceManager(ILogger<ComponentStatePersistenceManager> logger)
2731
{
28-
State = new ComponentApplicationState(_currentState, _pauseCallbacks);
32+
State = new PersistentComponentState(_currentState, _pauseCallbacks);
2933
_logger = logger;
3034
}
3135

3236
/// <summary>
33-
/// Gets the <see cref="ComponentApplicationState"/> associated with the <see cref="ComponentApplicationLifetime"/>.
37+
/// Gets the <see cref="ComponentStatePersistenceManager"/> associated with the <see cref="ComponentStatePersistenceManager"/>.
3438
/// </summary>
35-
public ComponentApplicationState State { get; }
39+
public PersistentComponentState State { get; }
3640

3741
/// <summary>
38-
/// Restores the component application state from the given <see cref="IComponentApplicationStateStore"/>.
42+
/// Restores the component application state from the given <see cref="IPersistentComponentStateStore"/>.
3943
/// </summary>
40-
/// <param name="store">The <see cref="IComponentApplicationStateStore"/> to restore the application state from.</param>
44+
/// <param name="store">The <see cref="IPersistentComponentStateStore"/> to restore the application state from.</param>
4145
/// <returns>A <see cref="Task"/> that will complete when the state has been restored.</returns>
42-
public async Task RestoreStateAsync(IComponentApplicationStateStore store)
46+
public async Task RestoreStateAsync(IPersistentComponentStateStore store)
4347
{
4448
var data = await store.GetPersistedStateAsync();
4549
State.InitializeExistingState(data);
4650
}
4751

4852
/// <summary>
49-
/// Persists the component application state into the given <see cref="IComponentApplicationStateStore"/>.
53+
/// Persists the component application state into the given <see cref="IPersistentComponentStateStore"/>.
5054
/// </summary>
51-
/// <param name="store">The <see cref="IComponentApplicationStateStore"/> to restore the application state from.</param>
55+
/// <param name="store">The <see cref="IPersistentComponentStateStore"/> to restore the application state from.</param>
5256
/// <param name="renderer">The <see cref="Renderer"/> that components are being rendered.</param>
5357
/// <returns>A <see cref="Task"/> that will complete when the state has been restored.</returns>
54-
public Task PersistStateAsync(IComponentApplicationStateStore store, Renderer renderer)
58+
public Task PersistStateAsync(IPersistentComponentStateStore store, Renderer renderer)
5559
{
5660
if (_stateIsPersisted)
5761
{
@@ -64,18 +68,31 @@ public Task PersistStateAsync(IComponentApplicationStateStore store, Renderer re
6468

6569
async Task PauseAndPersistState()
6670
{
71+
State.PersistingState = true;
6772
await PauseAsync();
73+
State.PersistingState = false;
74+
75+
var data = new Dictionary<string, ReadOnlySequence<byte>>(StringComparer.Ordinal);
76+
foreach (var (key, value) in _currentState)
77+
{
78+
data[key] = new ReadOnlySequence<byte>(value.WrittenMemory);
79+
}
6880

69-
var data = new ReadOnlyDictionary<string, byte[]>(_currentState);
7081
await store.PersistStateAsync(data);
82+
83+
foreach (var value in _currentState.Values)
84+
{
85+
value.Dispose();
86+
}
87+
_currentState.Clear();
7188
}
7289
}
7390

7491
internal Task PauseAsync()
7592
{
7693
List<Task>? pendingCallbackTasks = null;
7794

78-
for (int i = 0; i < _pauseCallbacks.Count; i++)
95+
for (var i = 0; i < _pauseCallbacks.Count; i++)
7996
{
8097
var callback = _pauseCallbacks[i];
8198
var result = ExecuteCallback(callback, _logger);
@@ -95,7 +112,7 @@ internal Task PauseAsync()
95112
return Task.CompletedTask;
96113
}
97114

98-
static Task ExecuteCallback(ComponentApplicationState.OnPersistingCallback callback, ILogger<ComponentApplicationLifetime> logger)
115+
static Task ExecuteCallback(Func<Task> callback, ILogger<ComponentStatePersistenceManager> logger)
99116
{
100117
try
101118
{
@@ -115,7 +132,7 @@ static Task ExecuteCallback(ComponentApplicationState.OnPersistingCallback callb
115132
return Task.CompletedTask;
116133
}
117134

118-
static async Task Awaited(Task task, ILogger<ComponentApplicationLifetime> logger)
135+
static async Task Awaited(Task task, ILogger<ComponentStatePersistenceManager> logger)
119136
{
120137
try
121138
{
@@ -129,5 +146,14 @@ static async Task Awaited(Task task, ILogger<ComponentApplicationLifetime> logge
129146
}
130147
}
131148
}
149+
150+
void IDisposable.Dispose()
151+
{
152+
foreach (var value in _currentState.Values)
153+
{
154+
value.Dispose();
155+
}
156+
_currentState.Clear();
157+
}
132158
}
133159
}

src/Components/Components/src/Microsoft.AspNetCore.Components.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<Compile Include="$(ComponentsSharedSourceRoot)src\JsonSerializerOptionsProvider.cs" />
1616
<Compile Include="$(SharedSourceRoot)LinkerFlags.cs" LinkBase="Shared" />
1717
<Compile Include="$(SharedSourceRoot)QueryStringEnumerable.cs" LinkBase="Shared" />
18+
<Compile Include="$(RepoRoot)src\Shared\Components\PooledByteBufferWritter.cs" LinkBase="Infrastructure" />
1819
</ItemGroup>
1920

2021
<ItemGroup>

src/Components/Components/src/Lifetime/ComponentApplicationState.cs renamed to src/Components/Components/src/PersistentComponentState.cs

Lines changed: 64 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,70 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.Collections.Generic;
4+
using System.Buffers;
65
using System.Diagnostics.CodeAnalysis;
76
using System.Text.Json;
8-
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Components.Infrastructure;
98
using static Microsoft.AspNetCore.Internal.LinkerFlags;
109

1110
namespace Microsoft.AspNetCore.Components
1211
{
1312
/// <summary>
1413
/// The state for the components and services of a components application.
1514
/// </summary>
16-
public class ComponentApplicationState
15+
public class PersistentComponentState
1716
{
18-
private IDictionary<string, byte[]>? _existingState;
19-
private readonly IDictionary<string, byte[]> _currentState;
20-
private readonly List<OnPersistingCallback> _registeredCallbacks;
17+
private IDictionary<string, ReadOnlySequence<byte>>? _existingState;
18+
private readonly IDictionary<string, PooledByteBufferWriter> _currentState;
2119

22-
internal ComponentApplicationState(
23-
IDictionary<string, byte[]> currentState,
24-
List<OnPersistingCallback> pauseCallbacks)
20+
private readonly List<Func<Task>> _registeredCallbacks;
21+
22+
internal PersistentComponentState(
23+
IDictionary<string, PooledByteBufferWriter> currentState,
24+
List<Func<Task>> pauseCallbacks)
2525
{
2626
_currentState = currentState;
2727
_registeredCallbacks = pauseCallbacks;
2828
}
2929

30-
internal void InitializeExistingState(IDictionary<string, byte[]> existingState)
30+
internal bool PersistingState { get; set; }
31+
32+
internal void InitializeExistingState(IDictionary<string, ReadOnlySequence<byte>> existingState)
3133
{
3234
if (_existingState != null)
3335
{
34-
throw new InvalidOperationException("ComponentApplicationState already initialized.");
36+
throw new InvalidOperationException("PersistentComponentState already initialized.");
3537
}
3638
_existingState = existingState ?? throw new ArgumentNullException(nameof(existingState));
3739
}
3840

3941
/// <summary>
40-
/// Represents the method that performs operations when <see cref="OnPersisting"/> is raised and the application is about to be paused.
42+
/// Register a callback to persist the component state when the application is about to be paused.
43+
/// Registered callbacks can use this opportunity to persist their state so that it can be retrieved when the application resumes.
4144
/// </summary>
42-
/// <returns>A <see cref="Task"/> that will complete when the method is done preparing for the application pause.</returns>
43-
public delegate Task OnPersistingCallback();
44-
45-
/// <summary>
46-
/// An event that is raised when the application is about to be paused.
47-
/// Registered handlers can use this opportunity to persist their state so that it can be retrieved when the application resumes.
48-
/// </summary>
49-
public event OnPersistingCallback OnPersisting
45+
/// <param name="callback">The callback to invoke when the application is being paused.</param>
46+
/// <returns>A subscription that can be used to unregister the callback when disposed.</returns>
47+
public PersistingComponentStateSubscription RegisterOnPersisting(Func<Task> callback)
5048
{
51-
add
49+
if (callback == null)
5250
{
53-
if (value == null)
54-
{
55-
throw new ArgumentNullException(nameof(value));
56-
}
57-
58-
_registeredCallbacks.Add(value);
51+
throw new ArgumentNullException(nameof(callback));
5952
}
60-
remove
61-
{
62-
if (value == null)
63-
{
64-
throw new ArgumentNullException(nameof(value));
65-
}
6653

67-
_registeredCallbacks.Remove(value);
68-
}
54+
_registeredCallbacks.Add(callback);
55+
56+
return new PersistingComponentStateSubscription(_registeredCallbacks, callback);
6957
}
7058

7159
/// <summary>
7260
/// Tries to retrieve the persisted state with the given <paramref name="key"/>.
7361
/// When the key is present, the state is successfully returned via <paramref name="value"/>
74-
/// and removed from the <see cref="ComponentApplicationState"/>.
62+
/// and removed from the <see cref="PersistentComponentState"/>.
7563
/// </summary>
7664
/// <param name="key">The key used to persist the state.</param>
7765
/// <param name="value">The persisted state.</param>
7866
/// <returns><c>true</c> if the state was found; <c>false</c> otherwise.</returns>
79-
public bool TryTakePersistedState(string key, [MaybeNullWhen(false)] out byte[]? value)
67+
public bool TryTake(string key, out ReadOnlySequence<byte> value)
8068
{
8169
if (key is null)
8270
{
@@ -89,7 +77,7 @@ public bool TryTakePersistedState(string key, [MaybeNullWhen(false)] out byte[]?
8977
// and we don't want to fail in that case.
9078
// When a service is prerendering there is no state to restore and in other cases the host
9179
// is responsible for initializing the state before services or components can access it.
92-
value = null;
80+
value = default;
9381
return false;
9482
}
9583

@@ -105,27 +93,35 @@ public bool TryTakePersistedState(string key, [MaybeNullWhen(false)] out byte[]?
10593
}
10694

10795
/// <summary>
108-
/// Persists the serialized state <paramref name="value"/> for the given <paramref name="key"/>.
96+
/// Persists the serialized state <paramref name="valueWriter"/> for the given <paramref name="key"/>.
10997
/// </summary>
11098
/// <param name="key">The key to use to persist the state.</param>
111-
/// <param name="value">The state to persist.</param>
112-
public void PersistState(string key, byte[] value)
99+
/// <param name="valueWriter">The state to persist.</param>
100+
public void Persist(string key, Action<IBufferWriter<byte>> valueWriter)
113101
{
114102
if (key is null)
115103
{
116104
throw new ArgumentNullException(nameof(key));
117105
}
118106

119-
if (value is null)
107+
if (valueWriter is null)
108+
{
109+
throw new ArgumentNullException(nameof(valueWriter));
110+
}
111+
112+
if (!PersistingState)
120113
{
121-
throw new ArgumentNullException(nameof(value));
114+
throw new InvalidOperationException("Persisting state is only allowed during an OnPersisting callback.");
122115
}
123116

124117
if (_currentState.ContainsKey(key))
125118
{
126119
throw new ArgumentException($"There is already a persisted object under the same key '{key}'");
127120
}
128-
_currentState.Add(key, value);
121+
122+
var writer = new PooledByteBufferWriter();
123+
_currentState.Add(key, writer);
124+
valueWriter(writer);
129125
}
130126

131127
/// <summary>
@@ -142,29 +138,47 @@ public void PersistState(string key, byte[] value)
142138
throw new ArgumentNullException(nameof(key));
143139
}
144140

145-
PersistState(key, JsonSerializer.SerializeToUtf8Bytes(instance, JsonSerializerOptionsProvider.Options));
141+
if (key is null)
142+
{
143+
throw new ArgumentNullException(nameof(key));
144+
}
145+
146+
if (!PersistingState)
147+
{
148+
throw new InvalidOperationException("Persisting state is only allowed during an OnPersisting callback.");
149+
}
150+
151+
if (_currentState.ContainsKey(key))
152+
{
153+
throw new ArgumentException($"There is already a persisted object under the same key '{key}'");
154+
}
155+
156+
var writer = new PooledByteBufferWriter();
157+
_currentState.Add(key, writer);
158+
JsonSerializer.Serialize(new Utf8JsonWriter(writer), instance, JsonSerializerOptionsProvider.Options);
146159
}
147160

148161
/// <summary>
149162
/// Tries to retrieve the persisted state as JSON with the given <paramref name="key"/> and deserializes it into an
150163
/// instance of type <typeparamref name="TValue"/>.
151164
/// When the key is present, the state is successfully returned via <paramref name="instance"/>
152-
/// and removed from the <see cref="ComponentApplicationState"/>.
165+
/// and removed from the <see cref="PersistentComponentState"/>.
153166
/// </summary>
154167
/// <param name="key">The key used to persist the instance.</param>
155168
/// <param name="instance">The persisted instance.</param>
156169
/// <returns><c>true</c> if the state was found; <c>false</c> otherwise.</returns>
157170
[RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")]
158-
public bool TryTakeAsJson<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string key, [MaybeNullWhen(false)] out TValue? instance)
171+
public bool TryTakeFromJson<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string key, [MaybeNullWhen(false)] out TValue? instance)
159172
{
160173
if (key is null)
161174
{
162175
throw new ArgumentNullException(nameof(key));
163176
}
164177

165-
if (TryTakePersistedState(key, out var data))
178+
if (TryTake(key, out var data))
166179
{
167-
instance = JsonSerializer.Deserialize<TValue>(data, JsonSerializerOptionsProvider.Options)!;
180+
var reader = new Utf8JsonReader(data);
181+
instance = JsonSerializer.Deserialize<TValue>(ref reader, JsonSerializerOptionsProvider.Options)!;
168182
return true;
169183
}
170184
else

0 commit comments

Comments
 (0)