Skip to content

Commit d7ed4d9

Browse files
Copilotjaviercn
andcommitted
Implement interface architecture changes for custom serializers per feedback
- Remove CancellationToken from IPersistentComponentStateSerializer interface - Create internal IPersistentComponentStateSerializer base interface with Type parameter - Add default interface implementations for type safety - Add serializer caching with ConcurrentDictionary - Move serializer resolution outside of lambda for better performance - Add PersistAsBytes and TryTakeBytes methods for raw byte operations - Update PublicAPI to reflect interface changes Co-authored-by: javiercn <[email protected]>
1 parent 147b5bc commit d7ed4d9

File tree

5 files changed

+116
-28
lines changed

5 files changed

+116
-28
lines changed

src/Components/Components/src/IPersistentComponentStateSerializer.cs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,43 @@
55

66
namespace Microsoft.AspNetCore.Components;
77

8+
/// <summary>
9+
/// Provides custom serialization logic for persistent component state values.
10+
/// </summary>
11+
public interface IPersistentComponentStateSerializer
12+
{
13+
/// <summary>
14+
/// Serializes the provided <paramref name="value"/> and writes it to the <paramref name="writer"/>.
15+
/// </summary>
16+
/// <param name="type">The type of the value to serialize.</param>
17+
/// <param name="value">The value to serialize.</param>
18+
/// <param name="writer">The buffer writer to write the serialized data to.</param>
19+
/// <returns>A task that represents the asynchronous serialization operation.</returns>
20+
Task PersistAsync(Type type, object value, IBufferWriter<byte> writer);
21+
22+
/// <summary>
23+
/// Deserializes a value from the provided <paramref name="data"/>.
24+
/// This method must be synchronous to avoid UI tearing during component state restoration.
25+
/// </summary>
26+
/// <param name="type">The type of the value to deserialize.</param>
27+
/// <param name="data">The serialized data to deserialize.</param>
28+
/// <returns>The deserialized value.</returns>
29+
object Restore(Type type, ReadOnlySequence<byte> data);
30+
}
31+
832
/// <summary>
933
/// Provides custom serialization logic for persistent component state values of type <typeparamref name="T"/>.
1034
/// </summary>
1135
/// <typeparam name="T">The type of the value to serialize.</typeparam>
12-
public interface IPersistentComponentStateSerializer<T>
36+
public interface IPersistentComponentStateSerializer<T> : IPersistentComponentStateSerializer
1337
{
1438
/// <summary>
1539
/// Serializes the provided <paramref name="value"/> and writes it to the <paramref name="writer"/>.
1640
/// </summary>
1741
/// <param name="value">The value to serialize.</param>
1842
/// <param name="writer">The buffer writer to write the serialized data to.</param>
19-
/// <param name="cancellationToken">A cancellation token that can be used to cancel the serialization operation.</param>
2043
/// <returns>A task that represents the asynchronous serialization operation.</returns>
21-
Task PersistAsync(T value, IBufferWriter<byte> writer, CancellationToken cancellationToken);
44+
Task PersistAsync(T value, IBufferWriter<byte> writer);
2245

2346
/// <summary>
2447
/// Deserializes a value of type <typeparamref name="T"/> from the provided <paramref name="data"/>.
@@ -27,4 +50,16 @@ public interface IPersistentComponentStateSerializer<T>
2750
/// <param name="data">The serialized data to deserialize.</param>
2851
/// <returns>The deserialized value.</returns>
2952
T Restore(ReadOnlySequence<byte> data);
53+
54+
/// <summary>
55+
/// Default implementation of the non-generic PersistAsync method.
56+
/// </summary>
57+
Task IPersistentComponentStateSerializer.PersistAsync(Type type, object value, IBufferWriter<byte> writer)
58+
=> PersistAsync((T)value, writer);
59+
60+
/// <summary>
61+
/// Default implementation of the non-generic Restore method.
62+
/// </summary>
63+
object IPersistentComponentStateSerializer.Restore(Type type, ReadOnlySequence<byte> data)
64+
=> Restore(data)!;
3065
}

src/Components/Components/src/PersistentComponentState.cs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,36 @@ internal void PersistAsJson(string key, object instance, [DynamicallyAccessedMem
111111
_currentState.Add(key, JsonSerializer.SerializeToUtf8Bytes(instance, type, JsonSerializerOptionsProvider.Options));
112112
}
113113

114+
/// <summary>
115+
/// Persists the provided byte array under the given key.
116+
/// </summary>
117+
/// <param name="key">The key to use to persist the state.</param>
118+
/// <param name="data">The byte array to persist.</param>
119+
internal void PersistAsBytes(string key, byte[] data)
120+
{
121+
ArgumentNullException.ThrowIfNull(key);
122+
123+
if (!PersistingState)
124+
{
125+
throw new InvalidOperationException("Persisting state is only allowed during an OnPersisting callback.");
126+
}
127+
128+
if (_currentState.ContainsKey(key))
129+
{
130+
throw new ArgumentException($"There is already a persisted object under the same key '{key}'");
131+
}
132+
133+
_currentState.Add(key, data);
134+
}
135+
114136
/// <summary>
115137
/// Serializes <paramref name="instance"/> using the provided <paramref name="serializer"/> and persists it under the given <paramref name="key"/>.
116138
/// </summary>
117139
/// <typeparam name="TValue">The <paramref name="instance"/> type.</typeparam>
118140
/// <param name="key">The key to use to persist the state.</param>
119141
/// <param name="instance">The instance to persist.</param>
120142
/// <param name="serializer">The custom serializer to use for serialization.</param>
121-
/// <param name="cancellationToken">A cancellation token that can be used to cancel the serialization operation.</param>
122-
internal async Task PersistAsync<TValue>(string key, TValue instance, IPersistentComponentStateSerializer<TValue> serializer, CancellationToken cancellationToken = default)
143+
internal async Task PersistAsync<TValue>(string key, TValue instance, IPersistentComponentStateSerializer<TValue> serializer)
123144
{
124145
ArgumentNullException.ThrowIfNull(key);
125146
ArgumentNullException.ThrowIfNull(serializer);
@@ -135,7 +156,7 @@ internal async Task PersistAsync<TValue>(string key, TValue instance, IPersisten
135156
}
136157

137158
using var writer = new PooledArrayBufferWriter<byte>();
138-
await serializer.PersistAsync(instance, writer, cancellationToken);
159+
await serializer.PersistAsync(instance, writer);
139160
_currentState.Add(key, writer.WrittenMemory.ToArray());
140161
}
141162

@@ -212,6 +233,19 @@ internal bool TryTake<TValue>(string key, IPersistentComponentStateSerializer<TV
212233
}
213234
}
214235

236+
/// <summary>
237+
/// Tries to retrieve the persisted state as raw bytes with the given <paramref name="key"/>.
238+
/// When the key is present, the raw bytes are successfully returned via <paramref name="data"/>
239+
/// and removed from the <see cref="PersistentComponentState"/>.
240+
/// </summary>
241+
/// <param name="key">The key used to persist the data.</param>
242+
/// <param name="data">The persisted raw bytes.</param>
243+
/// <returns><c>true</c> if the state was found; <c>false</c> otherwise.</returns>
244+
internal bool TryTakeBytes(string key, [MaybeNullWhen(false)] out byte[]? data)
245+
{
246+
return TryTake(key, out data);
247+
}
248+
215249
private bool TryTake(string key, out byte[]? value)
216250
{
217251
ArgumentNullException.ThrowIfNull(key);

src/Components/Components/src/PersistentStateValueProvider.cs

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ internal sealed class PersistentStateValueProvider(PersistentComponentState stat
1919
{
2020
private static readonly ConcurrentDictionary<(string, string, string), byte[]> _keyCache = new();
2121
private static readonly ConcurrentDictionary<(Type, string), PropertyGetter> _propertyGetterCache = new();
22+
private static readonly ConcurrentDictionary<Type, IPersistentComponentStateSerializer?> _serializerCache = new();
2223

2324
private readonly Dictionary<ComponentState, PersistingComponentStateSubscription> _subscriptions = [];
2425

@@ -43,19 +44,16 @@ public bool CanSupplyValue(in CascadingParameterInfo parameterInfo)
4344
var storageKey = ComputeKey(componentState, parameterInfo.PropertyName);
4445

4546
// Try to get a custom serializer for this type first
46-
var serializerType = typeof(IPersistentComponentStateSerializer<>).MakeGenericType(parameterInfo.PropertyType);
47-
var customSerializer = serviceProvider.GetService(serializerType);
47+
var customSerializer = ResolveSerializer(parameterInfo.PropertyType);
4848

4949
if (customSerializer != null)
5050
{
51-
// Use reflection to call the generic TryTake method with the custom serializer
52-
var tryTakeMethod = typeof(PersistentComponentState).GetMethod(nameof(PersistentComponentState.TryTake), BindingFlags.Instance | BindingFlags.NonPublic, [typeof(string), serializerType, parameterInfo.PropertyType.MakeByRefType()]);
53-
if (tryTakeMethod != null)
51+
if (state.TryTakeBytes(storageKey, out var data))
5452
{
55-
var parameters = new object?[] { storageKey, customSerializer, null };
56-
var success = (bool)tryTakeMethod.Invoke(state, parameters)!;
57-
return success ? parameters[2] : null;
53+
var sequence = new ReadOnlySequence<byte>(data!);
54+
return customSerializer.Restore(parameterInfo.PropertyType, sequence);
5855
}
56+
return null;
5957
}
6058

6159
// Fallback to JSON serialization
@@ -69,6 +67,10 @@ public void Subscribe(ComponentState subscriber, in CascadingParameterInfo param
6967
{
7068
var propertyName = parameterInfo.PropertyName;
7169
var propertyType = parameterInfo.PropertyType;
70+
71+
// Resolve serializer outside the lambda
72+
var customSerializer = ResolveSerializer(propertyType);
73+
7274
_subscriptions[subscriber] = state.RegisterOnPersisting(async () =>
7375
{
7476
var storageKey = ComputeKey(subscriber, propertyName);
@@ -79,20 +81,12 @@ public void Subscribe(ComponentState subscriber, in CascadingParameterInfo param
7981
return;
8082
}
8183

82-
// Try to get a custom serializer for this type first
83-
var serializerType = typeof(IPersistentComponentStateSerializer<>).MakeGenericType(propertyType);
84-
var customSerializer = serviceProvider.GetService(serializerType);
85-
8684
if (customSerializer != null)
8785
{
88-
// Use reflection to call the generic PersistAsync method with the custom serializer
89-
var persistMethod = typeof(PersistentComponentState).GetMethod(nameof(PersistentComponentState.PersistAsync), BindingFlags.Instance | BindingFlags.NonPublic, [typeof(string), propertyType, serializerType, typeof(CancellationToken)]);
90-
if (persistMethod != null)
91-
{
92-
var task = (Task)persistMethod.Invoke(state, [storageKey, property, customSerializer, CancellationToken.None])!;
93-
await task;
94-
return;
95-
}
86+
using var writer = new PooledArrayBufferWriter<byte>();
87+
await customSerializer.PersistAsync(propertyType, property, writer);
88+
state.PersistAsBytes(storageKey, writer.WrittenMemory.ToArray());
89+
return;
9690
}
9791

9892
// Fallback to JSON serialization
@@ -105,6 +99,28 @@ private static PropertyGetter ResolvePropertyGetter(Type type, string propertyNa
10599
return _propertyGetterCache.GetOrAdd((type, propertyName), PropertyGetterFactory);
106100
}
107101

102+
private IPersistentComponentStateSerializer? ResolveSerializer(Type type)
103+
{
104+
if (_serializerCache.TryGetValue(type, out var cached))
105+
{
106+
return cached;
107+
}
108+
109+
var serializer = SerializerFactory(type);
110+
if (serializer != null)
111+
{
112+
_serializerCache.TryAdd(type, serializer);
113+
}
114+
return serializer;
115+
}
116+
117+
private IPersistentComponentStateSerializer? SerializerFactory(Type type)
118+
{
119+
var serializerType = typeof(IPersistentComponentStateSerializer<>).MakeGenericType(type);
120+
var serializer = serviceProvider.GetService(serializerType);
121+
return serializer as IPersistentComponentStateSerializer;
122+
}
123+
108124
[UnconditionalSuppressMessage(
109125
"Trimming",
110126
"IL2077:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The source field does not have matching annotations.",

src/Components/Components/src/PublicAPI.Unshipped.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCo
2121
static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
2222
static Microsoft.AspNetCore.Components.Infrastructure.PersistentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
2323
virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.GetComponentKey() -> object?
24+
Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer
25+
Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.PersistAsync(System.Type! type, object! value, System.Buffers.IBufferWriter<byte>! writer) -> System.Threading.Tasks.Task!
26+
Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.Restore(System.Type! type, System.Buffers.ReadOnlySequence<byte> data) -> object!
2427
Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer<T>
25-
Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer<T>.PersistAsync(T value, System.Buffers.IBufferWriter<byte>! writer, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
28+
Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer<T>.PersistAsync(T value, System.Buffers.IBufferWriter<byte>! writer) -> System.Threading.Tasks.Task!
2629
Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer<T>.Restore(System.Buffers.ReadOnlySequence<byte> data) -> T

src/Components/Components/test/IPersistentComponentStateSerializerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public void TryTake_CanUseCustomSerializer()
5959

6060
private class TestStringSerializer : IPersistentComponentStateSerializer<string>
6161
{
62-
public Task PersistAsync(string value, IBufferWriter<byte> writer, CancellationToken cancellationToken)
62+
public Task PersistAsync(string value, IBufferWriter<byte> writer)
6363
{
6464
var bytes = Encoding.UTF8.GetBytes(value);
6565
writer.Write(bytes);

0 commit comments

Comments
 (0)