Skip to content

Commit 7820b1b

Browse files
committed
Address feedback
1 parent bb934d0 commit 7820b1b

11 files changed

+59
-66
lines changed

src/Components/Server/src/Circuits/CircuitHost.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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.ObjectModel;
45
using System.Diagnostics;
56
using System.Globalization;
67
using System.Linq;
@@ -161,7 +162,7 @@ public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, A
161162
// web service, preventing UI updates.
162163
if (Descriptors.Count > 0)
163164
{
164-
store.ExistingState.Clear();
165+
store.ExistingState = ReadOnlyDictionary<string, byte[]>.Empty;
165166
}
166167

167168
// This variable is used to track that this is the first time we are updating components.
@@ -821,7 +822,7 @@ internal Task UpdateRootComponents(
821822
// At this point all components have successfully produced an initial render and we can clear the contents of the component
822823
// application state store. This ensures the memory that was not used during the initial render of these components gets
823824
// reclaimed since no-one else is holding on to it any longer.
824-
store.ExistingState.Clear();
825+
store.ExistingState = ReadOnlyDictionary<string, byte[]>.Empty;
825826
}
826827
}
827828
});

src/Components/Server/src/Circuits/CircuitPersistenceManager.cs

Lines changed: 38 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -16,45 +16,23 @@ internal partial class CircuitPersistenceManager(
1616
ServerComponentSerializer serverComponentSerializer,
1717
ICircuitPersistenceProvider circuitPersistenceProvider)
1818
{
19-
private const string CircuitPersistenceManagerKey = $"Microsoft.AspNetCore.Components.Server.Circuits.{nameof(CircuitPersistenceManager)}";
20-
2119
public async Task PauseCircuitAsync(CircuitHost circuit, CancellationToken cancellation = default)
2220
{
2321
var renderer = circuit.Renderer;
2422
var persistenceManager = circuit.Services.GetRequiredService<ComponentStatePersistenceManager>();
23+
var collector = new CircuitPersistenceManagerCollector(circuitOptions, serverComponentSerializer, circuit.Renderer);
2524
using var subscription = persistenceManager.State.RegisterOnPersisting(
26-
() => PersistRootComponents(renderer, persistenceManager.State),
25+
collector.PersistRootComponents,
2726
RenderMode.InteractiveServer);
28-
var store = new CircuitPersistenceManagerStore();
29-
await persistenceManager.PersistStateAsync(store, renderer);
27+
28+
await persistenceManager.PersistStateAsync(collector, renderer);
3029

3130
await circuitPersistenceProvider.PersistCircuitAsync(
3231
circuit.CircuitId,
33-
store.PersistedCircuitState,
32+
collector.PersistedCircuitState,
3433
cancellation);
3534
}
3635

37-
private Task PersistRootComponents(RemoteRenderer renderer, PersistentComponentState state)
38-
{
39-
var persistedComponents = new Dictionary<int, ComponentMarker>();
40-
var components = renderer.GetOrCreateWebRootComponentManager().GetRootComponents();
41-
var invocation = new ServerComponentInvocationSequence();
42-
foreach (var (id, componentKey, (componentType, parameters)) in components)
43-
{
44-
var distributedRetention = circuitOptions.Value.PersistedCircuitInMemoryRetentionPeriod;
45-
var localRetention = circuitOptions.Value.PersistedCircuitInMemoryRetentionPeriod;
46-
var maxRetention = distributedRetention > localRetention ? distributedRetention : localRetention;
47-
48-
var marker = ComponentMarker.Create(ComponentMarker.ServerMarkerType, false, componentKey);
49-
serverComponentSerializer.SerializeInvocation(ref marker, invocation, componentType, parameters, maxRetention);
50-
persistedComponents.Add(id, marker);
51-
}
52-
53-
state.PersistAsJson(CircuitPersistenceManagerKey, persistedComponents);
54-
55-
return Task.CompletedTask;
56-
}
57-
5836
public async Task<PersistedCircuitState> ResumeCircuitAsync(CircuitId circuitId, CancellationToken cancellation = default)
5937
{
6038
return await circuitPersistenceProvider.RestoreCircuitAsync(circuitId, cancellation);
@@ -63,7 +41,7 @@ public async Task<PersistedCircuitState> ResumeCircuitAsync(CircuitId circuitId,
6341
// We are going to construct a RootComponentOperationBatch but we are going to replace the descriptors from the client with the
6442
// descriptors that we have persisted when pausing the circuit.
6543
// The way pausing and resuming works is that when the client starts the resume process, it 'simulates' that an SSR has happened and
66-
// queues and 'Add' operation for each server-side component that is on the document.
44+
// queues an 'Add' operation for each server-side component that is on the document.
6745
// That ends up calling UpdateRootComponents with the old descriptors and no application state.
6846
// On the server side, we replace the descriptors with the ones that we have persisted. We can't use the original descriptors because
6947
// those have a lifetime of ~ 5 minutes, after which we are not able to unprotect them anymore.
@@ -139,10 +117,40 @@ static Dictionary<int, ComponentMarker> TryDeserializeMarkers(byte[] rootCompone
139117
}
140118
}
141119

142-
private class CircuitPersistenceManagerStore : IPersistentComponentStateStore
120+
private class CircuitPersistenceManagerCollector(
121+
IOptions<CircuitOptions> circuitOptions,
122+
ServerComponentSerializer serverComponentSerializer,
123+
RemoteRenderer renderer)
124+
: IPersistentComponentStateStore
143125
{
144126
internal PersistedCircuitState PersistedCircuitState { get; private set; }
145127

128+
public Task PersistRootComponents()
129+
{
130+
var persistedComponents = new Dictionary<int, ComponentMarker>();
131+
var components = renderer.GetOrCreateWebRootComponentManager().GetRootComponents();
132+
var invocation = new ServerComponentInvocationSequence();
133+
foreach (var (id, componentKey, (componentType, parameters)) in components)
134+
{
135+
var distributedRetention = circuitOptions.Value.PersistedCircuitInMemoryRetentionPeriod;
136+
var localRetention = circuitOptions.Value.PersistedCircuitInMemoryRetentionPeriod;
137+
var maxRetention = distributedRetention > localRetention ? distributedRetention : localRetention;
138+
139+
var marker = ComponentMarker.Create(ComponentMarker.ServerMarkerType, prerendered: false, componentKey);
140+
serverComponentSerializer.SerializeInvocation(ref marker, invocation, componentType, parameters, maxRetention);
141+
persistedComponents.Add(id, marker);
142+
}
143+
144+
PersistedCircuitState = new PersistedCircuitState
145+
{
146+
RootComponents = JsonSerializer.SerializeToUtf8Bytes(
147+
persistedComponents,
148+
CircuitPersistenceManagerSerializerContext.Default.DictionaryInt32ComponentMarker)
149+
};
150+
151+
return Task.CompletedTask;
152+
}
153+
146154
// This store only support serializing the state
147155
Task<IDictionary<string, byte[]>> IPersistentComponentStateStore.GetPersistedStateAsync() => throw new NotImplementedException();
148156

@@ -152,26 +160,7 @@ private class CircuitPersistenceManagerStore : IPersistentComponentStateStore
152160
// and store them separately from the other state.
153161
Task IPersistentComponentStateStore.PersistStateAsync(IReadOnlyDictionary<string, byte[]> state)
154162
{
155-
var dictionary = new Dictionary<string, byte[]>(state.Count - 1);
156-
byte[] rootComponentMarkers = null;
157-
foreach (var (key, value) in state)
158-
{
159-
if (key == CircuitPersistenceManagerKey)
160-
{
161-
rootComponentMarkers = value;
162-
}
163-
else
164-
{
165-
dictionary[key] = value;
166-
}
167-
}
168-
169-
PersistedCircuitState = new PersistedCircuitState
170-
{
171-
ApplicationState = dictionary,
172-
RootComponents = rootComponentMarkers
173-
};
174-
163+
PersistedCircuitState.ApplicationState = state;
175164
return Task.CompletedTask;
176165
}
177166
}

src/Components/Server/src/Circuits/DefaultPersistedCircuitCache.cs renamed to src/Components/Server/src/Circuits/DefaultInMemoryCircuitPersistenceProvider.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ internal sealed partial class DefaultInMemoryCircuitPersistenceProvider : ICircu
1616
private readonly Lock _lock = new();
1717
private readonly CircuitOptions _options;
1818
private readonly MemoryCache _persistedCircuits;
19-
private readonly Task<PersistedCircuitState> _noMatch = Task.FromResult<PersistedCircuitState>(null);
19+
private static readonly Task<PersistedCircuitState> _noMatch = Task.FromResult<PersistedCircuitState>(null);
2020
private readonly ILogger<ICircuitPersistenceProvider> _logger;
2121

2222
public PostEvictionCallbackRegistration PostEvictionCallback { get; internal set; }
@@ -66,7 +66,8 @@ private void PersistCore(CircuitId circuitId, PersistedCircuitState persistedCir
6666
var persistedCircuitEntry = new PersistedCircuitEntry
6767
{
6868
State = persistedCircuitState,
69-
TokenSource = cancellationTokenSource
69+
TokenSource = cancellationTokenSource,
70+
CircuitId = circuitId
7071
};
7172

7273
_persistedCircuits.Set(circuitId.Secret, persistedCircuitEntry, options);
@@ -81,13 +82,13 @@ private void OnEntryEvicted(object key, object value, EvictionReason reason, obj
8182
// Happens after the circuit state times out, this is triggered by the CancellationTokenSource we register
8283
// with the entry, which is what controls the expiration
8384
case EvictionReason.Capacity:
84-
// Happens when the cache is full
85+
// Happens when the cache is full
8586
var persistedCircuitEntry = (PersistedCircuitEntry)value;
8687
Log.CircuitStateEvicted(_logger, persistedCircuitEntry.CircuitId, reason);
8788
break;
8889

8990
case EvictionReason.Removed:
90-
// Happens when the entry is explicitly removed as part of resuming a circuit.
91+
// Happens when the entry is explicitly removed as part of resuming a circuit.
9192
return;
9293
default:
9394
Debug.Fail($"Unexpected {nameof(EvictionReason)} {reason}");

src/Components/Server/src/Circuits/IServerComponentDeserializer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ bool TryDeserializeComponentDescriptorCollection(
1212
out List<ComponentDescriptor> descriptors);
1313
bool TryDeserializeRootComponentOperations(string serializedComponentOperations, [NotNullWhen(true)] out RootComponentOperationBatch? operationBatch, bool deserializeDescriptors = true);
1414

15-
public bool TryDeserializeWebRootComponentDescriptor(ComponentMarker record, [NotNullWhen(true)] out WebRootComponentDescriptor? result);
15+
bool TryDeserializeWebRootComponentDescriptor(ComponentMarker record, [NotNullWhen(true)] out WebRootComponentDescriptor? result);
1616
}

src/Components/Server/src/Circuits/PersistedCircuitState.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits;
88
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
99
internal class PersistedCircuitState
1010
{
11-
public Dictionary<string, byte[]> ApplicationState { get; internal set; }
11+
public IReadOnlyDictionary<string, byte[]> ApplicationState { get; internal set; }
1212

1313
public byte[] RootComponents { get; internal set; }
1414

src/Components/Server/src/ComponentHub.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ public async ValueTask<string> ResumeCircuit(
312312
}
313313

314314
PersistedCircuitState? persistedCircuitState;
315-
if (rootComponents == "[]" && string.IsNullOrEmpty(applicationState))
315+
if (RootComponentIsEmpty(rootComponents) && string.IsNullOrEmpty(applicationState))
316316
{
317317
persistedCircuitState = await _circuitPersistenceManager.ResumeCircuitAsync(circuitId, Context.ConnectionAborted);
318318
if (persistedCircuitState == null)

src/Components/Server/test/Circuits/ComponentHubTest.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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.ObjectModel;
45
using System.Diagnostics.CodeAnalysis;
56
using System.Security.Claims;
67
using System.Text.RegularExpressions;
@@ -164,7 +165,7 @@ public async Task CanCallUpdateRootComponentsOnResumedCircuit()
164165
.ReturnsAsync(new PersistedCircuitState
165166
{
166167
RootComponents = [.. """{}"""u8],
167-
ApplicationState = []
168+
ApplicationState = ReadOnlyDictionary<string, byte[]>.Empty
168169
});
169170

170171
var (mockClientProxy, hub) = InitializeComponentHub(deserializer, handleRegistryMock.Object, providerMock.Object);
@@ -264,7 +265,7 @@ public async Task CanResumeAppWhenPersistedComponentStateIsAvailable()
264265
.ReturnsAsync(new PersistedCircuitState
265266
{
266267
RootComponents = [],
267-
ApplicationState = []
268+
ApplicationState = ReadOnlyDictionary<string, byte[]>.Empty,
268269
});
269270

270271
var (mockClientProxy, hub) = InitializeComponentHub(null, handleRegistryMock.Object, providerMock.Object);

src/Components/Server/test/Circuits/DefaultPersistedCircuitCacheTest.cs renamed to src/Components/Server/test/Circuits/DefaultInMemoryCircuitPersistenceProviderTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits;
1111

12-
public class DefaultPersistedCircuitCacheTest
12+
public class DefaultInMemoryCircuitPersistenceProviderTest
1313
{
1414
[Fact]
1515
public async Task PersistCircuitAsync_StoresCircuitState()

src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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.ObjectModel;
45
using System.Diagnostics.CodeAnalysis;
56
using System.Reflection.Metadata;
67
using Microsoft.AspNetCore.Components.Infrastructure;
@@ -204,7 +205,7 @@ internal async Task RunAsyncCore(CancellationToken cancellationToken, WebAssembl
204205
});
205206

206207
await initializationTcs.Task;
207-
store.ExistingState.Clear();
208+
store.ExistingState = ReadOnlyDictionary<string, byte[]>.Empty;
208209

209210
await tcs.Task;
210211
}

src/Shared/Components/PrerenderComponentApplicationStore.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ internal class PrerenderComponentApplicationStore : IPersistentComponentStateSto
1616

1717
public PrerenderComponentApplicationStore()
1818
{
19-
ExistingState = new();
19+
ExistingState = new Dictionary<string, byte[]>();
2020
}
2121

2222
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", Justification = "Simple deserialize of primitive types.")]
@@ -41,7 +41,7 @@ protected void DeserializeState(byte[] existingState)
4141
public string? PersistedState { get; private set; }
4242
#nullable disable
4343

44-
public Dictionary<string, byte[]> ExistingState { get; protected set; }
44+
public IReadOnlyDictionary<string, byte[]> ExistingState { get; protected internal set; }
4545

4646
public Task<IDictionary<string, byte[]>> GetPersistedStateAsync()
4747
{

src/Shared/Components/ProtectedPrerenderComponentApplicationStore.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ public ProtectedPrerenderComponentApplicationStore(string existingState, IDataPr
2121
DeserializeState(_protector.Unprotect(Convert.FromBase64String(existingState)));
2222
}
2323

24-
public ProtectedPrerenderComponentApplicationStore(Dictionary<string, byte[]> deserializedState, IDataProtectionProvider dataProtectionProvider)
24+
public ProtectedPrerenderComponentApplicationStore(IReadOnlyDictionary<string, byte[]> deserializedState, IDataProtectionProvider dataProtectionProvider)
2525
{
2626
CreateProtector(dataProtectionProvider);
27-
ExistingState = deserializedState;
27+
ExistingState = new Dictionary<string, byte[]>(deserializedState);
2828
}
2929

3030
protected override byte[] SerializeState(IReadOnlyDictionary<string, byte[]> state)

0 commit comments

Comments
 (0)