From e2d06027fd8ade66bb1002b74a300b4ce59580a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 15:55:10 +0000 Subject: [PATCH 01/22] Initial plan From 726d6f56dbd567a9f6a2c8974bcaa01056b11d63 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:01:55 +0000 Subject: [PATCH 02/22] Initial planning for persistent component state serialization extensibility Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- activate.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 activate.sh diff --git a/activate.sh b/activate.sh old mode 100644 new mode 100755 From efb1c3ba82a2784dac9cc5d7c9cabb52d69f77d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:12:42 +0000 Subject: [PATCH 03/22] Implement IPersistentComponentStateSerializer interface and core functionality Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../IPersistentComponentStateSerializer.cs | 30 ++++++++ .../Microsoft.AspNetCore.Components.csproj | 1 + .../src/PersistentComponentState.cs | 57 +++++++++++++++ .../src/PersistentStateValueProvider.cs | 42 ++++++++++- .../Components/src/PublicAPI.Unshipped.txt | 5 ++ ...PersistentComponentStateSerializerTests.cs | 73 +++++++++++++++++++ .../test/PersistentStateValueProviderTests.cs | 30 ++++---- src/Shared/PooledArrayBufferWriter.cs | 2 +- 8 files changed, 220 insertions(+), 20 deletions(-) create mode 100644 src/Components/Components/src/IPersistentComponentStateSerializer.cs create mode 100644 src/Components/Components/test/IPersistentComponentStateSerializerTests.cs diff --git a/src/Components/Components/src/IPersistentComponentStateSerializer.cs b/src/Components/Components/src/IPersistentComponentStateSerializer.cs new file mode 100644 index 000000000000..4f711b700128 --- /dev/null +++ b/src/Components/Components/src/IPersistentComponentStateSerializer.cs @@ -0,0 +1,30 @@ +// 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; + +/// +/// Provides custom serialization logic for persistent component state values of type . +/// +/// The type of the value to serialize. +public interface IPersistentComponentStateSerializer +{ + /// + /// Serializes the provided and writes it to the . + /// + /// The value to serialize. + /// The buffer writer to write the serialized data to. + /// A cancellation token that can be used to cancel the serialization operation. + /// A task that represents the asynchronous serialization operation. + Task PersistAsync(T value, IBufferWriter writer, CancellationToken cancellationToken); + + /// + /// Deserializes a value of type from the provided . + /// This method must be synchronous to avoid UI tearing during component state restoration. + /// + /// The serialized data to deserialize. + /// The deserialized value. + T Restore(ReadOnlySequence data); +} \ No newline at end of file diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index ea4689586d69..8d36fe78ad80 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Components/Components/src/PersistentComponentState.cs b/src/Components/Components/src/PersistentComponentState.cs index a3dd2fdddc81..f2e4237bbd62 100644 --- a/src/Components/Components/src/PersistentComponentState.cs +++ b/src/Components/Components/src/PersistentComponentState.cs @@ -1,6 +1,7 @@ // 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; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -110,6 +111,34 @@ internal void PersistAsJson(string key, object instance, [DynamicallyAccessedMem _currentState.Add(key, JsonSerializer.SerializeToUtf8Bytes(instance, type, JsonSerializerOptionsProvider.Options)); } + /// + /// Serializes using the provided and persists it under the given . + /// + /// The type. + /// The key to use to persist the state. + /// The instance to persist. + /// The custom serializer to use for serialization. + /// A cancellation token that can be used to cancel the serialization operation. + public async Task PersistAsync(string key, TValue instance, IPersistentComponentStateSerializer serializer, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(key); + ArgumentNullException.ThrowIfNull(serializer); + + 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}'"); + } + + using var writer = new PooledArrayBufferWriter(); + await serializer.PersistAsync(instance, writer, cancellationToken); + _currentState.Add(key, writer.WrittenMemory.ToArray()); + } + /// /// Tries to retrieve the persisted state as JSON with the given and deserializes it into an /// instance of type . @@ -155,6 +184,34 @@ internal bool TryTakeFromJson(string key, [DynamicallyAccessedMembers(JsonSerial } } + /// + /// Tries to retrieve the persisted state with the given and deserializes it using the provided into an + /// instance of type . + /// When the key is present, the state is successfully returned via + /// and removed from the . + /// + /// The key used to persist the instance. + /// The custom serializer to use for deserialization. + /// The persisted instance. + /// true if the state was found; false otherwise. + public bool TryTake(string key, IPersistentComponentStateSerializer serializer, [MaybeNullWhen(false)] out TValue instance) + { + ArgumentNullException.ThrowIfNull(key); + ArgumentNullException.ThrowIfNull(serializer); + + if (TryTake(key, out var data)) + { + var sequence = new ReadOnlySequence(data!); + instance = serializer.Restore(sequence); + return true; + } + else + { + instance = default; + return false; + } + } + private bool TryTake(string key, out byte[]? value) { ArgumentNullException.ThrowIfNull(key); diff --git a/src/Components/Components/src/PersistentStateValueProvider.cs b/src/Components/Components/src/PersistentStateValueProvider.cs index 0ebaa2a34577..9dac807aafbb 100644 --- a/src/Components/Components/src/PersistentStateValueProvider.cs +++ b/src/Components/Components/src/PersistentStateValueProvider.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Components.Infrastructure; -internal sealed class PersistentStateValueProvider(PersistentComponentState state) : ICascadingValueSupplier +internal sealed class PersistentStateValueProvider(PersistentComponentState state, IServiceProvider serviceProvider) : ICascadingValueSupplier { private static readonly ConcurrentDictionary<(string, string, string), byte[]> _keyCache = new(); private static readonly ConcurrentDictionary<(Type, string), PropertyGetter> _propertyGetterCache = new(); @@ -42,6 +42,23 @@ public bool CanSupplyValue(in CascadingParameterInfo parameterInfo) var componentState = (ComponentState)key!; var storageKey = ComputeKey(componentState, parameterInfo.PropertyName); + // Try to get a custom serializer for this type first + var serializerType = typeof(IPersistentComponentStateSerializer<>).MakeGenericType(parameterInfo.PropertyType); + var customSerializer = serviceProvider.GetService(serializerType); + + if (customSerializer != null) + { + // Use reflection to call the generic TryTake method with the custom serializer + var tryTakeMethod = typeof(PersistentComponentState).GetMethod(nameof(PersistentComponentState.TryTake), BindingFlags.Instance | BindingFlags.Public, [typeof(string), serializerType, parameterInfo.PropertyType.MakeByRefType()]); + if (tryTakeMethod != null) + { + var parameters = new object?[] { storageKey, customSerializer, null }; + var success = (bool)tryTakeMethod.Invoke(state, parameters)!; + return success ? parameters[2] : null; + } + } + + // Fallback to JSON serialization return state.TryTakeFromJson(storageKey, parameterInfo.PropertyType, out var value) ? value : null; } @@ -52,17 +69,34 @@ public void Subscribe(ComponentState subscriber, in CascadingParameterInfo param { var propertyName = parameterInfo.PropertyName; var propertyType = parameterInfo.PropertyType; - _subscriptions[subscriber] = state.RegisterOnPersisting(() => + _subscriptions[subscriber] = state.RegisterOnPersisting(async () => { var storageKey = ComputeKey(subscriber, propertyName); var propertyGetter = ResolvePropertyGetter(subscriber.Component.GetType(), propertyName); var property = propertyGetter.GetValue(subscriber.Component); if (property == null) { - return Task.CompletedTask; + return; } + + // Try to get a custom serializer for this type first + var serializerType = typeof(IPersistentComponentStateSerializer<>).MakeGenericType(propertyType); + var customSerializer = serviceProvider.GetService(serializerType); + + if (customSerializer != null) + { + // Use reflection to call the generic PersistAsync method with the custom serializer + var persistMethod = typeof(PersistentComponentState).GetMethod(nameof(PersistentComponentState.PersistAsync), BindingFlags.Instance | BindingFlags.Public, [typeof(string), propertyType, serializerType, typeof(CancellationToken)]); + if (persistMethod != null) + { + var task = (Task)persistMethod.Invoke(state, [storageKey, property, customSerializer, CancellationToken.None])!; + await task; + return; + } + } + + // Fallback to JSON serialization state.PersistAsJson(storageKey, property, propertyType); - return Task.CompletedTask; }, subscriber.Renderer.GetComponentRenderMode(subscriber.Component)); } diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index f11613946794..a472b223c436 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -21,3 +21,8 @@ static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCo static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.AspNetCore.Components.Infrastructure.PersistentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.GetComponentKey() -> object? +Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer +Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.PersistAsync(T value, System.Buffers.IBufferWriter! writer, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.Restore(System.Buffers.ReadOnlySequence data) -> T +Microsoft.AspNetCore.Components.PersistentComponentState.PersistAsync(string! key, TValue instance, Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer! serializer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.PersistentComponentState.TryTake(string! key, Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer! serializer, out TValue instance) -> bool diff --git a/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs b/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs new file mode 100644 index 000000000000..0c6d0336041a --- /dev/null +++ b/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs @@ -0,0 +1,73 @@ +// 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; +using System.Text; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Components; + +public class IPersistentComponentStateSerializerTests +{ + [Fact] + public async Task PersistAsync_CanUseCustomSerializer() + { + // Arrange + var currentState = new Dictionary(); + var state = new PersistentComponentState(currentState, []); + var customSerializer = new TestStringSerializer(); + var testValue = "Hello, World!"; + + state.PersistingState = true; + + // Act + await state.PersistAsync("test-key", testValue, customSerializer); + + // Assert + state.PersistingState = false; + + // Simulate the state transfer that happens between persist and restore phases + var newState = new PersistentComponentState(new Dictionary(), []); + newState.InitializeExistingState(currentState); + + Assert.True(newState.TryTake("test-key", customSerializer, out var retrievedValue)); + Assert.Equal(testValue, retrievedValue); + } + + [Fact] + public void TryTake_CanUseCustomSerializer() + { + // Arrange + var customData = "Custom Data"; + var customBytes = Encoding.UTF8.GetBytes(customData); + var existingState = new Dictionary { { "test-key", customBytes } }; + + var state = new PersistentComponentState(new Dictionary(), []); + state.InitializeExistingState(existingState); + + var customSerializer = new TestStringSerializer(); + + // Act + var success = state.TryTake("test-key", customSerializer, out var retrievedValue); + + // Assert + Assert.True(success); + Assert.Equal(customData, retrievedValue); + } + + private class TestStringSerializer : IPersistentComponentStateSerializer + { + public Task PersistAsync(string value, IBufferWriter writer, CancellationToken cancellationToken) + { + var bytes = Encoding.UTF8.GetBytes(value); + writer.Write(bytes); + return Task.CompletedTask; + } + + public string Restore(ReadOnlySequence data) + { + var bytes = data.ToArray(); + return Encoding.UTF8.GetString(bytes); + } + } +} \ No newline at end of file diff --git a/src/Components/Components/test/PersistentStateValueProviderTests.cs b/src/Components/Components/test/PersistentStateValueProviderTests.cs index 9ffd37eb8124..6f8fc48e968f 100644 --- a/src/Components/Components/test/PersistentStateValueProviderTests.cs +++ b/src/Components/Components/test/PersistentStateValueProviderTests.cs @@ -25,7 +25,7 @@ public void CanRestoreState_ForComponentWithProperties() new Dictionary(), []); - var provider = new PersistentStateValueProvider(state); + var provider = new PersistentStateValueProvider(state, new ServiceCollection().BuildServiceProvider()); var renderer = new TestRenderer(); var component = new TestComponent(); // Update the method call to match the correct signature @@ -53,7 +53,7 @@ public void Subscribe_RegistersPersistenceCallback() var state = new PersistentComponentState( new Dictionary(), []); - var provider = new PersistentStateValueProvider(state); + var provider = new PersistentStateValueProvider(state, new ServiceCollection().BuildServiceProvider()); var renderer = new TestRenderer(); var component = new TestComponent(); var componentStates = CreateComponentState(renderer, [(component, null)], null); @@ -75,7 +75,7 @@ public void Unsubscribe_RemovesCallbackFromRegisteredCallbacks() var state = new PersistentComponentState( new Dictionary(), []); - var provider = new PersistentStateValueProvider(state); + var provider = new PersistentStateValueProvider(state, new ServiceCollection().BuildServiceProvider()); var renderer = new TestRenderer(); var component = new TestComponent(); var componentStates = CreateComponentState(renderer, [(component, null)], null); @@ -108,7 +108,7 @@ public async Task PersistAsync_PersistsStateForSubscribedComponentProperties() var componentState = componentStates.First(); // Create the provider and subscribe the component - var provider = new PersistentStateValueProvider(persistenceManager.State); + var provider = new PersistentStateValueProvider(persistenceManager.State, new ServiceCollection().BuildServiceProvider()); var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string)); provider.Subscribe(componentState, cascadingParameterInfo); @@ -147,7 +147,7 @@ public async Task PersistAsync_UsesParentComponentType_WhenAvailable() var componentState = componentStates.First(); // Create the provider and subscribe the component - var provider = new PersistentStateValueProvider(persistenceManager.State); + var provider = new PersistentStateValueProvider(persistenceManager.State, new ServiceCollection().BuildServiceProvider()); var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string)); provider.Subscribe(componentState, cascadingParameterInfo); @@ -187,7 +187,7 @@ public async Task PersistAsync_CanPersistMultipleComponentsOfSameType_WhenParent var componentState2 = componentStates.Last(); // Create the provider and subscribe the component - var provider = new PersistentStateValueProvider(persistenceManager.State); + var provider = new PersistentStateValueProvider(persistenceManager.State, new ServiceCollection().BuildServiceProvider()); var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string)); provider.Subscribe(componentState1, cascadingParameterInfo); provider.Subscribe(componentState2, cascadingParameterInfo); @@ -260,7 +260,7 @@ public async Task PersistAsync_CanPersistMultipleComponentsOfSameType_SupportsDi var componentState2 = componentStates.Last(); // Create the provider and subscribe the component - var provider = new PersistentStateValueProvider(persistenceManager.State); + var provider = new PersistentStateValueProvider(persistenceManager.State, new ServiceCollection().BuildServiceProvider()); var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string)); provider.Subscribe(componentState1, cascadingParameterInfo); provider.Subscribe(componentState2, cascadingParameterInfo); @@ -305,7 +305,7 @@ public async Task PersistenceFails_IfMultipleComponentsOfSameType_TryToPersistDa var componentState2 = componentStates.Last(); // Create the provider and subscribe the components - var provider = new PersistentStateValueProvider(persistenceManager.State); + var provider = new PersistentStateValueProvider(persistenceManager.State, new ServiceCollection().BuildServiceProvider()); var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string)); provider.Subscribe(componentState1, cascadingParameterInfo); provider.Subscribe(componentState2, cascadingParameterInfo); @@ -346,7 +346,7 @@ public async Task PersistentceFails_IfMultipleComponentsOfSameType_TryToPersistD var componentState2 = componentStates.Last(); // Create the provider and subscribe the components - var provider = new PersistentStateValueProvider(persistenceManager.State); + var provider = new PersistentStateValueProvider(persistenceManager.State, new ServiceCollection().BuildServiceProvider()); var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string)); provider.Subscribe(componentState1, cascadingParameterInfo); provider.Subscribe(componentState2, cascadingParameterInfo); @@ -379,7 +379,7 @@ public async Task PersistenceFails_MultipleComponentsUseTheSameKey() var componentState2 = componentStates.Last(); // Create the provider and subscribe the components - var provider = new PersistentStateValueProvider(persistenceManager.State); + var provider = new PersistentStateValueProvider(persistenceManager.State, new ServiceCollection().BuildServiceProvider()); var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string)); provider.Subscribe(componentState1, cascadingParameterInfo); provider.Subscribe(componentState2, cascadingParameterInfo); @@ -419,7 +419,7 @@ public async Task PersistenceFails_MultipleComponentsUseInvalidKeyTypes(object c var componentState2 = componentStates.Last(); // Create the provider and subscribe the components - var provider = new PersistentStateValueProvider(persistenceManager.State); + var provider = new PersistentStateValueProvider(persistenceManager.State, new ServiceCollection().BuildServiceProvider()); var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(TestComponent.State), typeof(string)); provider.Subscribe(componentState1, cascadingParameterInfo); provider.Subscribe(componentState2, cascadingParameterInfo); @@ -448,7 +448,7 @@ public async Task PersistAsync_CanPersistValueTypes_IntProperty() var componentState = componentStates.First(); // Create the provider and subscribe the component - var provider = new PersistentStateValueProvider(persistenceManager.State); + var provider = new PersistentStateValueProvider(persistenceManager.State, new ServiceCollection().BuildServiceProvider()); var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ValueTypeTestComponent.IntValue), typeof(int)); provider.Subscribe(componentState, cascadingParameterInfo); @@ -483,7 +483,7 @@ public async Task PersistAsync_CanPersistValueTypes_NullableIntProperty() var componentState = componentStates.First(); // Create the provider and subscribe the component - var provider = new PersistentStateValueProvider(persistenceManager.State); + var provider = new PersistentStateValueProvider(persistenceManager.State, new ServiceCollection().BuildServiceProvider()); var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ValueTypeTestComponent.NullableIntValue), typeof(int?)); provider.Subscribe(componentState, cascadingParameterInfo); @@ -518,7 +518,7 @@ public async Task PersistAsync_CanPersistValueTypes_TupleProperty() var componentState = componentStates.First(); // Create the provider and subscribe the component - var provider = new PersistentStateValueProvider(persistenceManager.State); + var provider = new PersistentStateValueProvider(persistenceManager.State, new ServiceCollection().BuildServiceProvider()); var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ValueTypeTestComponent.TupleValue), typeof((string, int))); provider.Subscribe(componentState, cascadingParameterInfo); @@ -553,7 +553,7 @@ public async Task PersistAsync_CanPersistValueTypes_NullableTupleProperty() var componentState = componentStates.First(); // Create the provider and subscribe the component - var provider = new PersistentStateValueProvider(persistenceManager.State); + var provider = new PersistentStateValueProvider(persistenceManager.State, new ServiceCollection().BuildServiceProvider()); var cascadingParameterInfo = CreateCascadingParameterInfo(nameof(ValueTypeTestComponent.NullableTupleValue), typeof((string, int)?)); provider.Subscribe(componentState, cascadingParameterInfo); diff --git a/src/Shared/PooledArrayBufferWriter.cs b/src/Shared/PooledArrayBufferWriter.cs index 9fad4a0a404a..cf580650d4ce 100644 --- a/src/Shared/PooledArrayBufferWriter.cs +++ b/src/Shared/PooledArrayBufferWriter.cs @@ -93,7 +93,7 @@ public void Dispose() ClearHelper(); ArrayPool.Shared.Return(_rentedBuffer); - _rentedBuffer = null; + _rentedBuffer = null!; } private void CheckIfDisposed() From 6f2f33c94f5481564613c9a45784c4a4aa2e0d42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:14:50 +0000 Subject: [PATCH 04/22] Add comprehensive tests and finalize serialization extensibility implementation Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../Components/test/IPersistentComponentStateSerializerTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs b/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs index 0c6d0336041a..526c4abf8b52 100644 --- a/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs +++ b/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs @@ -3,6 +3,8 @@ using System.Buffers; using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Components; From 147b5bc74f44ea5b285c54fb65d99f2bb707a4ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:26:18 +0000 Subject: [PATCH 05/22] Make PersistAsync and TryTake methods internal for custom serializers Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- src/Components/Components/src/PersistentComponentState.cs | 4 ++-- src/Components/Components/src/PersistentStateValueProvider.cs | 4 ++-- src/Components/Components/src/PublicAPI.Unshipped.txt | 2 -- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Components/Components/src/PersistentComponentState.cs b/src/Components/Components/src/PersistentComponentState.cs index f2e4237bbd62..c8a3f670c865 100644 --- a/src/Components/Components/src/PersistentComponentState.cs +++ b/src/Components/Components/src/PersistentComponentState.cs @@ -119,7 +119,7 @@ internal void PersistAsJson(string key, object instance, [DynamicallyAccessedMem /// The instance to persist. /// The custom serializer to use for serialization. /// A cancellation token that can be used to cancel the serialization operation. - public async Task PersistAsync(string key, TValue instance, IPersistentComponentStateSerializer serializer, CancellationToken cancellationToken = default) + internal async Task PersistAsync(string key, TValue instance, IPersistentComponentStateSerializer serializer, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(serializer); @@ -194,7 +194,7 @@ internal bool TryTakeFromJson(string key, [DynamicallyAccessedMembers(JsonSerial /// The custom serializer to use for deserialization. /// The persisted instance. /// true if the state was found; false otherwise. - public bool TryTake(string key, IPersistentComponentStateSerializer serializer, [MaybeNullWhen(false)] out TValue instance) + internal bool TryTake(string key, IPersistentComponentStateSerializer serializer, [MaybeNullWhen(false)] out TValue instance) { ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(serializer); diff --git a/src/Components/Components/src/PersistentStateValueProvider.cs b/src/Components/Components/src/PersistentStateValueProvider.cs index 9dac807aafbb..fd92c1a9726c 100644 --- a/src/Components/Components/src/PersistentStateValueProvider.cs +++ b/src/Components/Components/src/PersistentStateValueProvider.cs @@ -49,7 +49,7 @@ public bool CanSupplyValue(in CascadingParameterInfo parameterInfo) if (customSerializer != null) { // Use reflection to call the generic TryTake method with the custom serializer - var tryTakeMethod = typeof(PersistentComponentState).GetMethod(nameof(PersistentComponentState.TryTake), BindingFlags.Instance | BindingFlags.Public, [typeof(string), serializerType, parameterInfo.PropertyType.MakeByRefType()]); + var tryTakeMethod = typeof(PersistentComponentState).GetMethod(nameof(PersistentComponentState.TryTake), BindingFlags.Instance | BindingFlags.NonPublic, [typeof(string), serializerType, parameterInfo.PropertyType.MakeByRefType()]); if (tryTakeMethod != null) { var parameters = new object?[] { storageKey, customSerializer, null }; @@ -86,7 +86,7 @@ public void Subscribe(ComponentState subscriber, in CascadingParameterInfo param if (customSerializer != null) { // Use reflection to call the generic PersistAsync method with the custom serializer - var persistMethod = typeof(PersistentComponentState).GetMethod(nameof(PersistentComponentState.PersistAsync), BindingFlags.Instance | BindingFlags.Public, [typeof(string), propertyType, serializerType, typeof(CancellationToken)]); + var persistMethod = typeof(PersistentComponentState).GetMethod(nameof(PersistentComponentState.PersistAsync), BindingFlags.Instance | BindingFlags.NonPublic, [typeof(string), propertyType, serializerType, typeof(CancellationToken)]); if (persistMethod != null) { var task = (Task)persistMethod.Invoke(state, [storageKey, property, customSerializer, CancellationToken.None])!; diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index a472b223c436..b4a1c1685450 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -24,5 +24,3 @@ virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.GetComponentKey Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.PersistAsync(T value, System.Buffers.IBufferWriter! writer, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.Restore(System.Buffers.ReadOnlySequence data) -> T -Microsoft.AspNetCore.Components.PersistentComponentState.PersistAsync(string! key, TValue instance, Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer! serializer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Components.PersistentComponentState.TryTake(string! key, Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer! serializer, out TValue instance) -> bool From d7ed4d951ee5799c5cbe9ddfee62e77bcd21582c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:43:29 +0000 Subject: [PATCH 06/22] 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 <6995051+javiercn@users.noreply.github.com> --- .../IPersistentComponentStateSerializer.cs | 41 +++++++++++++- .../src/PersistentComponentState.cs | 40 ++++++++++++- .../src/PersistentStateValueProvider.cs | 56 ++++++++++++------- .../Components/src/PublicAPI.Unshipped.txt | 5 +- ...PersistentComponentStateSerializerTests.cs | 2 +- 5 files changed, 116 insertions(+), 28 deletions(-) diff --git a/src/Components/Components/src/IPersistentComponentStateSerializer.cs b/src/Components/Components/src/IPersistentComponentStateSerializer.cs index 4f711b700128..8d7717c2f535 100644 --- a/src/Components/Components/src/IPersistentComponentStateSerializer.cs +++ b/src/Components/Components/src/IPersistentComponentStateSerializer.cs @@ -5,20 +5,43 @@ namespace Microsoft.AspNetCore.Components; +/// +/// Provides custom serialization logic for persistent component state values. +/// +public interface IPersistentComponentStateSerializer +{ + /// + /// Serializes the provided and writes it to the . + /// + /// The type of the value to serialize. + /// The value to serialize. + /// The buffer writer to write the serialized data to. + /// A task that represents the asynchronous serialization operation. + Task PersistAsync(Type type, object value, IBufferWriter writer); + + /// + /// Deserializes a value from the provided . + /// This method must be synchronous to avoid UI tearing during component state restoration. + /// + /// The type of the value to deserialize. + /// The serialized data to deserialize. + /// The deserialized value. + object Restore(Type type, ReadOnlySequence data); +} + /// /// Provides custom serialization logic for persistent component state values of type . /// /// The type of the value to serialize. -public interface IPersistentComponentStateSerializer +public interface IPersistentComponentStateSerializer : IPersistentComponentStateSerializer { /// /// Serializes the provided and writes it to the . /// /// The value to serialize. /// The buffer writer to write the serialized data to. - /// A cancellation token that can be used to cancel the serialization operation. /// A task that represents the asynchronous serialization operation. - Task PersistAsync(T value, IBufferWriter writer, CancellationToken cancellationToken); + Task PersistAsync(T value, IBufferWriter writer); /// /// Deserializes a value of type from the provided . @@ -27,4 +50,16 @@ public interface IPersistentComponentStateSerializer /// The serialized data to deserialize. /// The deserialized value. T Restore(ReadOnlySequence data); + + /// + /// Default implementation of the non-generic PersistAsync method. + /// + Task IPersistentComponentStateSerializer.PersistAsync(Type type, object value, IBufferWriter writer) + => PersistAsync((T)value, writer); + + /// + /// Default implementation of the non-generic Restore method. + /// + object IPersistentComponentStateSerializer.Restore(Type type, ReadOnlySequence data) + => Restore(data)!; } \ No newline at end of file diff --git a/src/Components/Components/src/PersistentComponentState.cs b/src/Components/Components/src/PersistentComponentState.cs index c8a3f670c865..13cf942187b4 100644 --- a/src/Components/Components/src/PersistentComponentState.cs +++ b/src/Components/Components/src/PersistentComponentState.cs @@ -111,6 +111,28 @@ internal void PersistAsJson(string key, object instance, [DynamicallyAccessedMem _currentState.Add(key, JsonSerializer.SerializeToUtf8Bytes(instance, type, JsonSerializerOptionsProvider.Options)); } + /// + /// Persists the provided byte array under the given key. + /// + /// The key to use to persist the state. + /// The byte array to persist. + internal void PersistAsBytes(string key, byte[] data) + { + ArgumentNullException.ThrowIfNull(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}'"); + } + + _currentState.Add(key, data); + } + /// /// Serializes using the provided and persists it under the given . /// @@ -118,8 +140,7 @@ internal void PersistAsJson(string key, object instance, [DynamicallyAccessedMem /// The key to use to persist the state. /// The instance to persist. /// The custom serializer to use for serialization. - /// A cancellation token that can be used to cancel the serialization operation. - internal async Task PersistAsync(string key, TValue instance, IPersistentComponentStateSerializer serializer, CancellationToken cancellationToken = default) + internal async Task PersistAsync(string key, TValue instance, IPersistentComponentStateSerializer serializer) { ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(serializer); @@ -135,7 +156,7 @@ internal async Task PersistAsync(string key, TValue instance, IPersisten } using var writer = new PooledArrayBufferWriter(); - await serializer.PersistAsync(instance, writer, cancellationToken); + await serializer.PersistAsync(instance, writer); _currentState.Add(key, writer.WrittenMemory.ToArray()); } @@ -212,6 +233,19 @@ internal bool TryTake(string key, IPersistentComponentStateSerializer + /// Tries to retrieve the persisted state as raw bytes with the given . + /// When the key is present, the raw bytes are successfully returned via + /// and removed from the . + /// + /// The key used to persist the data. + /// The persisted raw bytes. + /// true if the state was found; false otherwise. + internal bool TryTakeBytes(string key, [MaybeNullWhen(false)] out byte[]? data) + { + return TryTake(key, out data); + } + private bool TryTake(string key, out byte[]? value) { ArgumentNullException.ThrowIfNull(key); diff --git a/src/Components/Components/src/PersistentStateValueProvider.cs b/src/Components/Components/src/PersistentStateValueProvider.cs index fd92c1a9726c..a9038a81fd96 100644 --- a/src/Components/Components/src/PersistentStateValueProvider.cs +++ b/src/Components/Components/src/PersistentStateValueProvider.cs @@ -19,6 +19,7 @@ internal sealed class PersistentStateValueProvider(PersistentComponentState stat { private static readonly ConcurrentDictionary<(string, string, string), byte[]> _keyCache = new(); private static readonly ConcurrentDictionary<(Type, string), PropertyGetter> _propertyGetterCache = new(); + private static readonly ConcurrentDictionary _serializerCache = new(); private readonly Dictionary _subscriptions = []; @@ -43,19 +44,16 @@ public bool CanSupplyValue(in CascadingParameterInfo parameterInfo) var storageKey = ComputeKey(componentState, parameterInfo.PropertyName); // Try to get a custom serializer for this type first - var serializerType = typeof(IPersistentComponentStateSerializer<>).MakeGenericType(parameterInfo.PropertyType); - var customSerializer = serviceProvider.GetService(serializerType); + var customSerializer = ResolveSerializer(parameterInfo.PropertyType); if (customSerializer != null) { - // Use reflection to call the generic TryTake method with the custom serializer - var tryTakeMethod = typeof(PersistentComponentState).GetMethod(nameof(PersistentComponentState.TryTake), BindingFlags.Instance | BindingFlags.NonPublic, [typeof(string), serializerType, parameterInfo.PropertyType.MakeByRefType()]); - if (tryTakeMethod != null) + if (state.TryTakeBytes(storageKey, out var data)) { - var parameters = new object?[] { storageKey, customSerializer, null }; - var success = (bool)tryTakeMethod.Invoke(state, parameters)!; - return success ? parameters[2] : null; + var sequence = new ReadOnlySequence(data!); + return customSerializer.Restore(parameterInfo.PropertyType, sequence); } + return null; } // Fallback to JSON serialization @@ -69,6 +67,10 @@ public void Subscribe(ComponentState subscriber, in CascadingParameterInfo param { var propertyName = parameterInfo.PropertyName; var propertyType = parameterInfo.PropertyType; + + // Resolve serializer outside the lambda + var customSerializer = ResolveSerializer(propertyType); + _subscriptions[subscriber] = state.RegisterOnPersisting(async () => { var storageKey = ComputeKey(subscriber, propertyName); @@ -79,20 +81,12 @@ public void Subscribe(ComponentState subscriber, in CascadingParameterInfo param return; } - // Try to get a custom serializer for this type first - var serializerType = typeof(IPersistentComponentStateSerializer<>).MakeGenericType(propertyType); - var customSerializer = serviceProvider.GetService(serializerType); - if (customSerializer != null) { - // Use reflection to call the generic PersistAsync method with the custom serializer - var persistMethod = typeof(PersistentComponentState).GetMethod(nameof(PersistentComponentState.PersistAsync), BindingFlags.Instance | BindingFlags.NonPublic, [typeof(string), propertyType, serializerType, typeof(CancellationToken)]); - if (persistMethod != null) - { - var task = (Task)persistMethod.Invoke(state, [storageKey, property, customSerializer, CancellationToken.None])!; - await task; - return; - } + using var writer = new PooledArrayBufferWriter(); + await customSerializer.PersistAsync(propertyType, property, writer); + state.PersistAsBytes(storageKey, writer.WrittenMemory.ToArray()); + return; } // Fallback to JSON serialization @@ -105,6 +99,28 @@ private static PropertyGetter ResolvePropertyGetter(Type type, string propertyNa return _propertyGetterCache.GetOrAdd((type, propertyName), PropertyGetterFactory); } + private IPersistentComponentStateSerializer? ResolveSerializer(Type type) + { + if (_serializerCache.TryGetValue(type, out var cached)) + { + return cached; + } + + var serializer = SerializerFactory(type); + if (serializer != null) + { + _serializerCache.TryAdd(type, serializer); + } + return serializer; + } + + private IPersistentComponentStateSerializer? SerializerFactory(Type type) + { + var serializerType = typeof(IPersistentComponentStateSerializer<>).MakeGenericType(type); + var serializer = serviceProvider.GetService(serializerType); + return serializer as IPersistentComponentStateSerializer; + } + [UnconditionalSuppressMessage( "Trimming", "IL2077:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The source field does not have matching annotations.", diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index b4a1c1685450..5a486aaa8513 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -21,6 +21,9 @@ static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCo static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.AspNetCore.Components.Infrastructure.PersistentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.GetComponentKey() -> object? +Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer +Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.PersistAsync(System.Type! type, object! value, System.Buffers.IBufferWriter! writer) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.Restore(System.Type! type, System.Buffers.ReadOnlySequence data) -> object! Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer -Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.PersistAsync(T value, System.Buffers.IBufferWriter! writer, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.PersistAsync(T value, System.Buffers.IBufferWriter! writer) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.Restore(System.Buffers.ReadOnlySequence data) -> T diff --git a/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs b/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs index 526c4abf8b52..925edfe5ff1e 100644 --- a/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs +++ b/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs @@ -59,7 +59,7 @@ public void TryTake_CanUseCustomSerializer() private class TestStringSerializer : IPersistentComponentStateSerializer { - public Task PersistAsync(string value, IBufferWriter writer, CancellationToken cancellationToken) + public Task PersistAsync(string value, IBufferWriter writer) { var bytes = Encoding.UTF8.GetBytes(value); writer.Write(bytes); From fc6a4d93b876e866146c05c2c48b4327c711ef6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:52:22 +0000 Subject: [PATCH 07/22] Fix serializer cache to be instance-based to prevent cross-test contamination - Changed _serializerCache from static to instance field to prevent different service providers from sharing cached serializers - This ensures each PersistentStateValueProvider instance has its own serializer cache Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../Components/src/PersistentStateValueProvider.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/Components/Components/src/PersistentStateValueProvider.cs b/src/Components/Components/src/PersistentStateValueProvider.cs index a9038a81fd96..a9a54581d7a6 100644 --- a/src/Components/Components/src/PersistentStateValueProvider.cs +++ b/src/Components/Components/src/PersistentStateValueProvider.cs @@ -19,7 +19,7 @@ internal sealed class PersistentStateValueProvider(PersistentComponentState stat { private static readonly ConcurrentDictionary<(string, string, string), byte[]> _keyCache = new(); private static readonly ConcurrentDictionary<(Type, string), PropertyGetter> _propertyGetterCache = new(); - private static readonly ConcurrentDictionary _serializerCache = new(); + private readonly ConcurrentDictionary _serializerCache = new(); private readonly Dictionary _subscriptions = []; @@ -101,17 +101,7 @@ private static PropertyGetter ResolvePropertyGetter(Type type, string propertyNa private IPersistentComponentStateSerializer? ResolveSerializer(Type type) { - if (_serializerCache.TryGetValue(type, out var cached)) - { - return cached; - } - - var serializer = SerializerFactory(type); - if (serializer != null) - { - _serializerCache.TryAdd(type, serializer); - } - return serializer; + return _serializerCache.GetOrAdd(type, SerializerFactory); } private IPersistentComponentStateSerializer? SerializerFactory(Type type) From 358f9e403292996a658cb353d0c61d17a8f09d09 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 17:19:03 +0000 Subject: [PATCH 08/22] Make IPersistentComponentStateSerializer interface internal per code review feedback Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../Components/src/IPersistentComponentStateSerializer.cs | 4 ++-- src/Components/Components/src/PublicAPI.Unshipped.txt | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Components/Components/src/IPersistentComponentStateSerializer.cs b/src/Components/Components/src/IPersistentComponentStateSerializer.cs index 8d7717c2f535..357614cd22d2 100644 --- a/src/Components/Components/src/IPersistentComponentStateSerializer.cs +++ b/src/Components/Components/src/IPersistentComponentStateSerializer.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components; /// /// Provides custom serialization logic for persistent component state values. /// -public interface IPersistentComponentStateSerializer +internal interface IPersistentComponentStateSerializer { /// /// Serializes the provided and writes it to the . @@ -33,7 +33,7 @@ public interface IPersistentComponentStateSerializer /// Provides custom serialization logic for persistent component state values of type . /// /// The type of the value to serialize. -public interface IPersistentComponentStateSerializer : IPersistentComponentStateSerializer +internal interface IPersistentComponentStateSerializer : IPersistentComponentStateSerializer { /// /// Serializes the provided and writes it to the . diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 5a486aaa8513..f11613946794 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -21,9 +21,3 @@ static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCo static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.AspNetCore.Components.Infrastructure.PersistentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.GetComponentKey() -> object? -Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer -Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.PersistAsync(System.Type! type, object! value, System.Buffers.IBufferWriter! writer) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.Restore(System.Type! type, System.Buffers.ReadOnlySequence data) -> object! -Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer -Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.PersistAsync(T value, System.Buffers.IBufferWriter! writer) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.Restore(System.Buffers.ReadOnlySequence data) -> T From 4c3dcac3a5bfbbaa85f3cb1762c4297ff185051a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 17:44:39 +0000 Subject: [PATCH 09/22] Make IPersistentComponentStateSerializer<T> public while keeping base interface internal Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../Components/src/IPersistentComponentStateSerializer.cs | 2 +- src/Components/Components/src/PublicAPI.Unshipped.txt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Components/Components/src/IPersistentComponentStateSerializer.cs b/src/Components/Components/src/IPersistentComponentStateSerializer.cs index 357614cd22d2..84f9df49d024 100644 --- a/src/Components/Components/src/IPersistentComponentStateSerializer.cs +++ b/src/Components/Components/src/IPersistentComponentStateSerializer.cs @@ -33,7 +33,7 @@ internal interface IPersistentComponentStateSerializer /// Provides custom serialization logic for persistent component state values of type . /// /// The type of the value to serialize. -internal interface IPersistentComponentStateSerializer : IPersistentComponentStateSerializer +public interface IPersistentComponentStateSerializer : IPersistentComponentStateSerializer { /// /// Serializes the provided and writes it to the . diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index f11613946794..4f5a360c8a5d 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -16,6 +16,9 @@ Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateS Microsoft.AspNetCore.Components.PersistentStateAttribute Microsoft.AspNetCore.Components.PersistentStateAttribute.PersistentStateAttribute() -> void Microsoft.AspNetCore.Components.Infrastructure.PersistentStateProviderServiceCollectionExtensions +Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer +Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.PersistAsync(T value, System.Buffers.IBufferWriter! writer) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.Restore(System.Buffers.ReadOnlySequence data) -> T static Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.AspNetCore.Components.IComponentRenderMode! componentRenderMode) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsMetrics(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! From df6eb65a641476e23ae515c6a7346ce6306a91ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 17:56:20 +0000 Subject: [PATCH 10/22] Move PersistAsync and TryTake methods to PersistentStateValueProvider per code review feedback Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../IPersistentComponentStateSerializer.cs | 2 +- .../src/PersistentComponentState.cs | 56 ------------------- .../src/PersistentStateValueProvider.cs | 45 +++++++++++++++ .../Components/src/PublicAPI.Unshipped.txt | 3 + ...PersistentComponentStateSerializerTests.cs | 11 +++- 5 files changed, 57 insertions(+), 60 deletions(-) diff --git a/src/Components/Components/src/IPersistentComponentStateSerializer.cs b/src/Components/Components/src/IPersistentComponentStateSerializer.cs index 84f9df49d024..8d7717c2f535 100644 --- a/src/Components/Components/src/IPersistentComponentStateSerializer.cs +++ b/src/Components/Components/src/IPersistentComponentStateSerializer.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components; /// /// Provides custom serialization logic for persistent component state values. /// -internal interface IPersistentComponentStateSerializer +public interface IPersistentComponentStateSerializer { /// /// Serializes the provided and writes it to the . diff --git a/src/Components/Components/src/PersistentComponentState.cs b/src/Components/Components/src/PersistentComponentState.cs index 13cf942187b4..d3f5e9fd9309 100644 --- a/src/Components/Components/src/PersistentComponentState.cs +++ b/src/Components/Components/src/PersistentComponentState.cs @@ -1,7 +1,6 @@ // 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; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -133,33 +132,6 @@ internal void PersistAsBytes(string key, byte[] data) _currentState.Add(key, data); } - /// - /// Serializes using the provided and persists it under the given . - /// - /// The type. - /// The key to use to persist the state. - /// The instance to persist. - /// The custom serializer to use for serialization. - internal async Task PersistAsync(string key, TValue instance, IPersistentComponentStateSerializer serializer) - { - ArgumentNullException.ThrowIfNull(key); - ArgumentNullException.ThrowIfNull(serializer); - - 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}'"); - } - - using var writer = new PooledArrayBufferWriter(); - await serializer.PersistAsync(instance, writer); - _currentState.Add(key, writer.WrittenMemory.ToArray()); - } - /// /// Tries to retrieve the persisted state as JSON with the given and deserializes it into an /// instance of type . @@ -205,34 +177,6 @@ internal bool TryTakeFromJson(string key, [DynamicallyAccessedMembers(JsonSerial } } - /// - /// Tries to retrieve the persisted state with the given and deserializes it using the provided into an - /// instance of type . - /// When the key is present, the state is successfully returned via - /// and removed from the . - /// - /// The key used to persist the instance. - /// The custom serializer to use for deserialization. - /// The persisted instance. - /// true if the state was found; false otherwise. - internal bool TryTake(string key, IPersistentComponentStateSerializer serializer, [MaybeNullWhen(false)] out TValue instance) - { - ArgumentNullException.ThrowIfNull(key); - ArgumentNullException.ThrowIfNull(serializer); - - if (TryTake(key, out var data)) - { - var sequence = new ReadOnlySequence(data!); - instance = serializer.Restore(sequence); - return true; - } - else - { - instance = default; - return false; - } - } - /// /// Tries to retrieve the persisted state as raw bytes with the given . /// When the key is present, the raw bytes are successfully returned via diff --git a/src/Components/Components/src/PersistentStateValueProvider.cs b/src/Components/Components/src/PersistentStateValueProvider.cs index a9a54581d7a6..9b2f8706d7c1 100644 --- a/src/Components/Components/src/PersistentStateValueProvider.cs +++ b/src/Components/Components/src/PersistentStateValueProvider.cs @@ -321,4 +321,49 @@ private static bool IsSerializableKey(object key) return result; } + + /// + /// Serializes using the provided and persists it under the given . + /// + /// The type. + /// The key to use to persist the state. + /// The instance to persist. + /// The custom serializer to use for serialization. + internal async Task PersistAsync(string key, TValue instance, IPersistentComponentStateSerializer serializer) + { + ArgumentNullException.ThrowIfNull(key); + ArgumentNullException.ThrowIfNull(serializer); + + using var writer = new PooledArrayBufferWriter(); + await serializer.PersistAsync(instance, writer); + state.PersistAsBytes(key, writer.WrittenMemory.ToArray()); + } + + /// + /// Tries to retrieve the persisted state with the given and deserializes it using the provided into an + /// instance of type . + /// When the key is present, the state is successfully returned via + /// and removed from the . + /// + /// The key used to persist the instance. + /// The custom serializer to use for deserialization. + /// The persisted instance. + /// true if the state was found; false otherwise. + internal bool TryTake(string key, IPersistentComponentStateSerializer serializer, [MaybeNullWhen(false)] out TValue instance) + { + ArgumentNullException.ThrowIfNull(key); + ArgumentNullException.ThrowIfNull(serializer); + + if (state.TryTakeBytes(key, out var data)) + { + var sequence = new ReadOnlySequence(data!); + instance = serializer.Restore(sequence); + return true; + } + else + { + instance = default; + return false; + } + } } diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 4f5a360c8a5d..cb3a8aaa4e32 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -16,6 +16,9 @@ Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateS Microsoft.AspNetCore.Components.PersistentStateAttribute Microsoft.AspNetCore.Components.PersistentStateAttribute.PersistentStateAttribute() -> void Microsoft.AspNetCore.Components.Infrastructure.PersistentStateProviderServiceCollectionExtensions +Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer +Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.PersistAsync(System.Type! type, object! value, System.Buffers.IBufferWriter! writer) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.Restore(System.Type! type, System.Buffers.ReadOnlySequence data) -> object! Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.PersistAsync(T value, System.Buffers.IBufferWriter! writer) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.Restore(System.Buffers.ReadOnlySequence data) -> T diff --git a/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs b/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs index 925edfe5ff1e..fac78895b623 100644 --- a/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs +++ b/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs @@ -17,13 +17,15 @@ public async Task PersistAsync_CanUseCustomSerializer() // Arrange var currentState = new Dictionary(); var state = new PersistentComponentState(currentState, []); + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var stateValueProvider = new PersistentStateValueProvider(state, serviceProvider); var customSerializer = new TestStringSerializer(); var testValue = "Hello, World!"; state.PersistingState = true; // Act - await state.PersistAsync("test-key", testValue, customSerializer); + await stateValueProvider.PersistAsync("test-key", testValue, customSerializer); // Assert state.PersistingState = false; @@ -31,8 +33,9 @@ public async Task PersistAsync_CanUseCustomSerializer() // Simulate the state transfer that happens between persist and restore phases var newState = new PersistentComponentState(new Dictionary(), []); newState.InitializeExistingState(currentState); + var newStateValueProvider = new PersistentStateValueProvider(newState, serviceProvider); - Assert.True(newState.TryTake("test-key", customSerializer, out var retrievedValue)); + Assert.True(newStateValueProvider.TryTake("test-key", customSerializer, out var retrievedValue)); Assert.Equal(testValue, retrievedValue); } @@ -47,10 +50,12 @@ public void TryTake_CanUseCustomSerializer() var state = new PersistentComponentState(new Dictionary(), []); state.InitializeExistingState(existingState); + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var stateValueProvider = new PersistentStateValueProvider(state, serviceProvider); var customSerializer = new TestStringSerializer(); // Act - var success = state.TryTake("test-key", customSerializer, out var retrievedValue); + var success = stateValueProvider.TryTake("test-key", customSerializer, out var retrievedValue); // Assert Assert.True(success); From cc5109efd34f3744d0581770afb13cdfc4ef3c25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 18:06:20 +0000 Subject: [PATCH 11/22] Add custom serializer support to E2E tests for persistent component state Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../ServerRenderingTests/InteractivityTest.cs | 4 ++ .../CustomIntSerializer.cs | 37 +++++++++++++++++++ .../RazorComponentEndpointsStartup.cs | 4 ++ .../DeclarativePersistStateComponent.razor | 8 ++++ 4 files changed, 53 insertions(+) create mode 100644 src/Components/test/testassets/Components.TestServer/CustomIntSerializer.cs diff --git a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs index 130b518aa210..c17a07065691 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs @@ -1059,6 +1059,7 @@ public void CanPersistPrerenderedStateDeclaratively_Server() Navigate($"{ServerPathBase}/persist-state?server=true&declarative=true"); Browser.Equal("restored", () => Browser.FindElement(By.Id("server")).Text); + Browser.Equal("42", () => Browser.FindElement(By.Id("custom-server")).Text); Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-server")).Text); } @@ -1077,6 +1078,7 @@ public void CanPersistPrerenderedStateDeclaratively_WebAssembly() Navigate($"{ServerPathBase}/persist-state?wasm=true&declarative=true"); Browser.Equal("restored", () => Browser.FindElement(By.Id("wasm")).Text); + Browser.Equal("42", () => Browser.FindElement(By.Id("custom-wasm")).Text); Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-wasm")).Text); } @@ -1095,6 +1097,7 @@ public void CanPersistPrerenderedStateDeclaratively_Auto_PersistsOnWebAssembly() Navigate($"{ServerPathBase}/persist-state?auto=true&declarative=true"); Browser.Equal("restored", () => Browser.FindElement(By.Id("auto")).Text); + Browser.Equal("42", () => Browser.FindElement(By.Id("custom-auto")).Text); Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("render-mode-auto")).Text); } @@ -1156,6 +1159,7 @@ public void CanPersistPrerenderedStateDeclaratively_Auto_PersistsOnServer() Navigate($"{ServerPathBase}/persist-state?auto=true&declarative=true"); Browser.Equal("restored", () => Browser.FindElement(By.Id("auto")).Text); + Browser.Equal("42", () => Browser.FindElement(By.Id("custom-auto")).Text); Browser.Equal("Server", () => Browser.FindElement(By.Id("render-mode-auto")).Text); } diff --git a/src/Components/test/testassets/Components.TestServer/CustomIntSerializer.cs b/src/Components/test/testassets/Components.TestServer/CustomIntSerializer.cs new file mode 100644 index 000000000000..4be462f43527 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/CustomIntSerializer.cs @@ -0,0 +1,37 @@ +// 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; +using System.Text; +using Microsoft.AspNetCore.Components; + +namespace TestServer; + +/// +/// A custom serializer for int values that uses a custom format to test serialization extensibility. +/// This serializer prefixes integer values with "CUSTOM:" to clearly distinguish them from JSON serialization. +/// +public class CustomIntSerializer : IPersistentComponentStateSerializer +{ + public Task PersistAsync(int value, IBufferWriter writer) + { + var customFormat = $"CUSTOM:{value}"; + var bytes = Encoding.UTF8.GetBytes(customFormat); + writer.Write(bytes); + return Task.CompletedTask; + } + + public int Restore(ReadOnlySequence data) + { + var bytes = data.ToArray(); + var text = Encoding.UTF8.GetString(bytes); + + if (text.StartsWith("CUSTOM:") && int.TryParse(text.Substring(7), out var value)) + { + return value; + } + + // Fallback to direct parsing if format is unexpected + return int.TryParse(text, out var fallbackValue) ? fallbackValue : 0; + } +} \ No newline at end of file diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index d545fe83461a..c43b30f10654 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -8,6 +8,7 @@ using Components.TestServer.RazorComponents; using Components.TestServer.RazorComponents.Pages.Forms; using Components.TestServer.Services; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Server; @@ -64,6 +65,9 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); + // Register custom serializer for E2E testing of persistent component state serialization extensibility + services.AddSingleton, CustomIntSerializer>(); + services.AddHttpContextAccessor(); services.AddSingleton(); services.AddCascadingAuthenticationState(); diff --git a/src/Components/test/testassets/TestContentPackage/DeclarativePersistStateComponent.razor b/src/Components/test/testassets/TestContentPackage/DeclarativePersistStateComponent.razor index 07cec64a2706..a0a8400c17cb 100644 --- a/src/Components/test/testassets/TestContentPackage/DeclarativePersistStateComponent.razor +++ b/src/Components/test/testassets/TestContentPackage/DeclarativePersistStateComponent.razor @@ -1,4 +1,5 @@ 

Application state is @Value

+

Custom value is @CustomValue

Render mode: @_renderMode

@code { @@ -11,11 +12,18 @@ [PersistentState] public string Value { get; set; } + [PersistentState] + public int CustomValue { get; set; } + private string _renderMode = "SSR"; protected override void OnInitialized() { Value ??= !RendererInfo.IsInteractive ? InitialValue : "not restored"; + if (CustomValue == 0) + { + CustomValue = !RendererInfo.IsInteractive ? 42 : 0; + } _renderMode = OperatingSystem.IsBrowser() ? "WebAssembly" : "Server"; } } From 208556834792d8591de986ac5c8e89f48071175b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 19:09:10 +0000 Subject: [PATCH 12/22] Fix CA1310 analyzer error by specifying StringComparison.Ordinal in CustomIntSerializer Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../testassets/Components.TestServer/CustomIntSerializer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/test/testassets/Components.TestServer/CustomIntSerializer.cs b/src/Components/test/testassets/Components.TestServer/CustomIntSerializer.cs index 4be462f43527..949dd1417376 100644 --- a/src/Components/test/testassets/Components.TestServer/CustomIntSerializer.cs +++ b/src/Components/test/testassets/Components.TestServer/CustomIntSerializer.cs @@ -26,7 +26,7 @@ public int Restore(ReadOnlySequence data) var bytes = data.ToArray(); var text = Encoding.UTF8.GetString(bytes); - if (text.StartsWith("CUSTOM:") && int.TryParse(text.Substring(7), out var value)) + if (text.StartsWith("CUSTOM:", StringComparison.Ordinal) && int.TryParse(text.Substring(7), out var value)) { return value; } From b6723a606ea5d5ea91f76c20c06ce13002fa825e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 3 Jul 2025 20:46:54 +0000 Subject: [PATCH 13/22] Move CustomIntSerializer to shared TestContentPackage and register in WebAssembly for E2E tests Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- src/Components/test/testassets/BasicTestApp/Program.cs | 4 ++++ .../Components.TestServer/RazorComponentEndpointsStartup.cs | 1 + .../CustomIntSerializer.cs | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) rename src/Components/test/testassets/{Components.TestServer => TestContentPackage}/CustomIntSerializer.cs (97%) diff --git a/src/Components/test/testassets/BasicTestApp/Program.cs b/src/Components/test/testassets/BasicTestApp/Program.cs index 554e65ba1ffe..4b7b70eae7d4 100644 --- a/src/Components/test/testassets/BasicTestApp/Program.cs +++ b/src/Components/test/testassets/BasicTestApp/Program.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Components.WebAssembly.Services; using Microsoft.Extensions.Logging.Configuration; using Microsoft.JSInterop; +using TestContentPackage; namespace BasicTestApp; @@ -45,6 +46,9 @@ public static async Task Main(string[] args) builder.Services.AddScoped(); builder.Services.AddTransient(); + // Register custom serializer for E2E testing of persistent component state serialization extensibility + builder.Services.AddSingleton, CustomIntSerializer>(); + builder.Services.AddKeyedSingleton("keyed-service-1", TestKeyedService.Create("value-1")); builder.Services.AddKeyedSingleton(TestServiceKey.ServiceB, TestKeyedService.Create("value-2")); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index c43b30f10654..ca9b936766f5 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Server; using Microsoft.AspNetCore.Mvc; +using TestContentPackage; using TestContentPackage.Services; namespace TestServer; diff --git a/src/Components/test/testassets/Components.TestServer/CustomIntSerializer.cs b/src/Components/test/testassets/TestContentPackage/CustomIntSerializer.cs similarity index 97% rename from src/Components/test/testassets/Components.TestServer/CustomIntSerializer.cs rename to src/Components/test/testassets/TestContentPackage/CustomIntSerializer.cs index 949dd1417376..a9a65cc8dc7e 100644 --- a/src/Components/test/testassets/Components.TestServer/CustomIntSerializer.cs +++ b/src/Components/test/testassets/TestContentPackage/CustomIntSerializer.cs @@ -5,7 +5,7 @@ using System.Text; using Microsoft.AspNetCore.Components; -namespace TestServer; +namespace TestContentPackage; /// /// A custom serializer for int values that uses a custom format to test serialization extensibility. From 67ca5ec5ed386c516440d379e383ad995d9b9ffe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 4 Jul 2025 03:26:52 +0000 Subject: [PATCH 14/22] Register CustomIntSerializer in Components.WasmMinimal for E2E test compatibility Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../test/testassets/Components.WasmMinimal/Program.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Components/test/testassets/Components.WasmMinimal/Program.cs b/src/Components/test/testassets/Components.WasmMinimal/Program.cs index 88a28726961b..ab9098babd5b 100644 --- a/src/Components/test/testassets/Components.WasmMinimal/Program.cs +++ b/src/Components/test/testassets/Components.WasmMinimal/Program.cs @@ -4,8 +4,10 @@ using System.Runtime.InteropServices.JavaScript; using System.Security.Claims; using Components.TestServer.Services; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using TestContentPackage; using TestContentPackage.Services; var builder = WebAssemblyHostBuilder.CreateDefault(args); @@ -14,6 +16,9 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); +// Register custom serializer for persistent component state +builder.Services.AddSingleton, CustomIntSerializer>(); + builder.Services.AddCascadingAuthenticationState(); builder.Services.AddAuthenticationStateDeserialization(options => From c1fefc54c9544561cf12284877b15f316cc02e48 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 4 Jul 2025 09:55:51 +0000 Subject: [PATCH 15/22] Make IPersistentComponentStateSerializer internal and move generic interface to separate file Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../IPersistentComponentStateSerializer.cs | 37 +------------------ .../IPersistentComponentStateSerializerOfT.cs | 29 +++++++++++++++ .../src/PersistentStateValueProvider.cs | 23 +++++++++++- .../Components/src/PublicAPI.Unshipped.txt | 3 -- 4 files changed, 52 insertions(+), 40 deletions(-) create mode 100644 src/Components/Components/src/IPersistentComponentStateSerializerOfT.cs diff --git a/src/Components/Components/src/IPersistentComponentStateSerializer.cs b/src/Components/Components/src/IPersistentComponentStateSerializer.cs index 8d7717c2f535..ce7ff0a7bbaf 100644 --- a/src/Components/Components/src/IPersistentComponentStateSerializer.cs +++ b/src/Components/Components/src/IPersistentComponentStateSerializer.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components; /// /// Provides custom serialization logic for persistent component state values. /// -public interface IPersistentComponentStateSerializer +internal interface IPersistentComponentStateSerializer { /// /// Serializes the provided and writes it to the . @@ -27,39 +27,4 @@ public interface IPersistentComponentStateSerializer /// The serialized data to deserialize. /// The deserialized value. object Restore(Type type, ReadOnlySequence data); -} - -/// -/// Provides custom serialization logic for persistent component state values of type . -/// -/// The type of the value to serialize. -public interface IPersistentComponentStateSerializer : IPersistentComponentStateSerializer -{ - /// - /// Serializes the provided and writes it to the . - /// - /// The value to serialize. - /// The buffer writer to write the serialized data to. - /// A task that represents the asynchronous serialization operation. - Task PersistAsync(T value, IBufferWriter writer); - - /// - /// Deserializes a value of type from the provided . - /// This method must be synchronous to avoid UI tearing during component state restoration. - /// - /// The serialized data to deserialize. - /// The deserialized value. - T Restore(ReadOnlySequence data); - - /// - /// Default implementation of the non-generic PersistAsync method. - /// - Task IPersistentComponentStateSerializer.PersistAsync(Type type, object value, IBufferWriter writer) - => PersistAsync((T)value, writer); - - /// - /// Default implementation of the non-generic Restore method. - /// - object IPersistentComponentStateSerializer.Restore(Type type, ReadOnlySequence data) - => Restore(data)!; } \ No newline at end of file diff --git a/src/Components/Components/src/IPersistentComponentStateSerializerOfT.cs b/src/Components/Components/src/IPersistentComponentStateSerializerOfT.cs new file mode 100644 index 000000000000..3af342d09cec --- /dev/null +++ b/src/Components/Components/src/IPersistentComponentStateSerializerOfT.cs @@ -0,0 +1,29 @@ +// 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; + +/// +/// Provides custom serialization logic for persistent component state values of type . +/// +/// The type of the value to serialize. +public interface IPersistentComponentStateSerializer +{ + /// + /// Serializes the provided and writes it to the . + /// + /// The value to serialize. + /// The buffer writer to write the serialized data to. + /// A task that represents the asynchronous serialization operation. + Task PersistAsync(T value, IBufferWriter writer); + + /// + /// Deserializes a value of type from the provided . + /// This method must be synchronous to avoid UI tearing during component state restoration. + /// + /// The serialized data to deserialize. + /// The deserialized value. + T Restore(ReadOnlySequence data); +} \ No newline at end of file diff --git a/src/Components/Components/src/PersistentStateValueProvider.cs b/src/Components/Components/src/PersistentStateValueProvider.cs index 9b2f8706d7c1..e5031d62855d 100644 --- a/src/Components/Components/src/PersistentStateValueProvider.cs +++ b/src/Components/Components/src/PersistentStateValueProvider.cs @@ -108,7 +108,15 @@ private static PropertyGetter ResolvePropertyGetter(Type type, string propertyNa { var serializerType = typeof(IPersistentComponentStateSerializer<>).MakeGenericType(type); var serializer = serviceProvider.GetService(serializerType); - return serializer as IPersistentComponentStateSerializer; + + if (serializer != null) + { + // Create an adapter that implements the internal interface + var adapterType = typeof(SerializerAdapter<>).MakeGenericType(type); + return (IPersistentComponentStateSerializer?)Activator.CreateInstance(adapterType, serializer); + } + + return null; } [UnconditionalSuppressMessage( @@ -366,4 +374,17 @@ internal bool TryTake(string key, IPersistentComponentStateSerializer + /// Adapter class to bridge between the public generic interface and the internal interface. + /// + /// The type of the value to serialize. + private sealed class SerializerAdapter(IPersistentComponentStateSerializer serializer) : IPersistentComponentStateSerializer + { + public Task PersistAsync(Type type, object value, IBufferWriter writer) + => serializer.PersistAsync((T)value, writer); + + public object Restore(Type type, ReadOnlySequence data) + => serializer.Restore(data)!; + } } diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index cb3a8aaa4e32..4f5a360c8a5d 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -16,9 +16,6 @@ Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateS Microsoft.AspNetCore.Components.PersistentStateAttribute Microsoft.AspNetCore.Components.PersistentStateAttribute.PersistentStateAttribute() -> void Microsoft.AspNetCore.Components.Infrastructure.PersistentStateProviderServiceCollectionExtensions -Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer -Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.PersistAsync(System.Type! type, object! value, System.Buffers.IBufferWriter! writer) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.Restore(System.Type! type, System.Buffers.ReadOnlySequence data) -> object! Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.PersistAsync(T value, System.Buffers.IBufferWriter! writer) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.Restore(System.Buffers.ReadOnlySequence data) -> T From cbf9fa9e351383151b20464f7df5c00108a717de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 4 Jul 2025 10:35:22 +0000 Subject: [PATCH 16/22] Implement explicit interface implementation for custom serializers and remove XML comments from internal interface Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../src/IPersistentComponentStateSerializer.cs | 18 ------------------ .../src/PersistentStateValueProvider.cs | 8 ++------ 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/src/Components/Components/src/IPersistentComponentStateSerializer.cs b/src/Components/Components/src/IPersistentComponentStateSerializer.cs index ce7ff0a7bbaf..9fe18f53ce92 100644 --- a/src/Components/Components/src/IPersistentComponentStateSerializer.cs +++ b/src/Components/Components/src/IPersistentComponentStateSerializer.cs @@ -5,26 +5,8 @@ namespace Microsoft.AspNetCore.Components; -/// -/// Provides custom serialization logic for persistent component state values. -/// internal interface IPersistentComponentStateSerializer { - /// - /// Serializes the provided and writes it to the . - /// - /// The type of the value to serialize. - /// The value to serialize. - /// The buffer writer to write the serialized data to. - /// A task that represents the asynchronous serialization operation. Task PersistAsync(Type type, object value, IBufferWriter writer); - - /// - /// Deserializes a value from the provided . - /// This method must be synchronous to avoid UI tearing during component state restoration. - /// - /// The type of the value to deserialize. - /// The serialized data to deserialize. - /// The deserialized value. object Restore(Type type, ReadOnlySequence data); } \ No newline at end of file diff --git a/src/Components/Components/src/PersistentStateValueProvider.cs b/src/Components/Components/src/PersistentStateValueProvider.cs index e5031d62855d..fe38b2c77f68 100644 --- a/src/Components/Components/src/PersistentStateValueProvider.cs +++ b/src/Components/Components/src/PersistentStateValueProvider.cs @@ -375,16 +375,12 @@ internal bool TryTake(string key, IPersistentComponentStateSerializer - /// Adapter class to bridge between the public generic interface and the internal interface. - /// - /// The type of the value to serialize. private sealed class SerializerAdapter(IPersistentComponentStateSerializer serializer) : IPersistentComponentStateSerializer { - public Task PersistAsync(Type type, object value, IBufferWriter writer) + Task IPersistentComponentStateSerializer.PersistAsync(Type type, object value, IBufferWriter writer) => serializer.PersistAsync((T)value, writer); - public object Restore(Type type, ReadOnlySequence data) + object IPersistentComponentStateSerializer.Restore(Type type, ReadOnlySequence data) => serializer.Restore(data)!; } } From 365064dcfc064c4b4e94547534824815b6a0ec54 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 4 Jul 2025 10:52:16 +0000 Subject: [PATCH 17/22] Implement explicit interface implementation in IPersistentComponentStateSerializer<T> and make base interface public Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../IPersistentComponentStateSerializer.cs | 2 +- .../IPersistentComponentStateSerializerOfT.cs | 14 +++++++++++++- .../src/PersistentStateValueProvider.cs | 19 ++----------------- .../Components/src/PublicAPI.Unshipped.txt | 3 +++ 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Components/Components/src/IPersistentComponentStateSerializer.cs b/src/Components/Components/src/IPersistentComponentStateSerializer.cs index 9fe18f53ce92..f6146749f24d 100644 --- a/src/Components/Components/src/IPersistentComponentStateSerializer.cs +++ b/src/Components/Components/src/IPersistentComponentStateSerializer.cs @@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.Components; -internal interface IPersistentComponentStateSerializer +public interface IPersistentComponentStateSerializer { Task PersistAsync(Type type, object value, IBufferWriter writer); object Restore(Type type, ReadOnlySequence data); diff --git a/src/Components/Components/src/IPersistentComponentStateSerializerOfT.cs b/src/Components/Components/src/IPersistentComponentStateSerializerOfT.cs index 3af342d09cec..5e3c311989e6 100644 --- a/src/Components/Components/src/IPersistentComponentStateSerializerOfT.cs +++ b/src/Components/Components/src/IPersistentComponentStateSerializerOfT.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Components; /// Provides custom serialization logic for persistent component state values of type . ///
/// The type of the value to serialize. -public interface IPersistentComponentStateSerializer +public interface IPersistentComponentStateSerializer : IPersistentComponentStateSerializer { /// /// Serializes the provided and writes it to the . @@ -26,4 +26,16 @@ public interface IPersistentComponentStateSerializer /// The serialized data to deserialize. /// The deserialized value. T Restore(ReadOnlySequence data); + + /// + /// Explicit interface implementation for non-generic serialization. + /// + Task IPersistentComponentStateSerializer.PersistAsync(Type type, object value, IBufferWriter writer) + => PersistAsync((T)value, writer); + + /// + /// Explicit interface implementation for non-generic deserialization. + /// + object IPersistentComponentStateSerializer.Restore(Type type, ReadOnlySequence data) + => Restore(data)!; } \ No newline at end of file diff --git a/src/Components/Components/src/PersistentStateValueProvider.cs b/src/Components/Components/src/PersistentStateValueProvider.cs index fe38b2c77f68..a7f59028e2a7 100644 --- a/src/Components/Components/src/PersistentStateValueProvider.cs +++ b/src/Components/Components/src/PersistentStateValueProvider.cs @@ -109,14 +109,8 @@ private static PropertyGetter ResolvePropertyGetter(Type type, string propertyNa var serializerType = typeof(IPersistentComponentStateSerializer<>).MakeGenericType(type); var serializer = serviceProvider.GetService(serializerType); - if (serializer != null) - { - // Create an adapter that implements the internal interface - var adapterType = typeof(SerializerAdapter<>).MakeGenericType(type); - return (IPersistentComponentStateSerializer?)Activator.CreateInstance(adapterType, serializer); - } - - return null; + // The generic interface now inherits from the internal interface, so we can cast directly + return serializer as IPersistentComponentStateSerializer; } [UnconditionalSuppressMessage( @@ -374,13 +368,4 @@ internal bool TryTake(string key, IPersistentComponentStateSerializer(IPersistentComponentStateSerializer serializer) : IPersistentComponentStateSerializer - { - Task IPersistentComponentStateSerializer.PersistAsync(Type type, object value, IBufferWriter writer) - => serializer.PersistAsync((T)value, writer); - - object IPersistentComponentStateSerializer.Restore(Type type, ReadOnlySequence data) - => serializer.Restore(data)!; - } } diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 4f5a360c8a5d..cb3a8aaa4e32 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -16,6 +16,9 @@ Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateS Microsoft.AspNetCore.Components.PersistentStateAttribute Microsoft.AspNetCore.Components.PersistentStateAttribute.PersistentStateAttribute() -> void Microsoft.AspNetCore.Components.Infrastructure.PersistentStateProviderServiceCollectionExtensions +Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer +Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.PersistAsync(System.Type! type, object! value, System.Buffers.IBufferWriter! writer) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.Restore(System.Type! type, System.Buffers.ReadOnlySequence data) -> object! Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.PersistAsync(T value, System.Buffers.IBufferWriter! writer) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.Restore(System.Buffers.ReadOnlySequence data) -> T From 3b3af1d29b4a5da69f3f452dd6fceeb43b0b00b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 4 Jul 2025 11:04:35 +0000 Subject: [PATCH 18/22] Remove redundant CustomIntSerializer registration from BasicTestApp Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- src/Components/test/testassets/BasicTestApp/Program.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Components/test/testassets/BasicTestApp/Program.cs b/src/Components/test/testassets/BasicTestApp/Program.cs index 4b7b70eae7d4..554e65ba1ffe 100644 --- a/src/Components/test/testassets/BasicTestApp/Program.cs +++ b/src/Components/test/testassets/BasicTestApp/Program.cs @@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Components.WebAssembly.Services; using Microsoft.Extensions.Logging.Configuration; using Microsoft.JSInterop; -using TestContentPackage; namespace BasicTestApp; @@ -46,9 +45,6 @@ public static async Task Main(string[] args) builder.Services.AddScoped(); builder.Services.AddTransient(); - // Register custom serializer for E2E testing of persistent component state serialization extensibility - builder.Services.AddSingleton, CustomIntSerializer>(); - builder.Services.AddKeyedSingleton("keyed-service-1", TestKeyedService.Create("value-1")); builder.Services.AddKeyedSingleton(TestServiceKey.ServiceB, TestKeyedService.Create("value-2")); From 970977b97718cbb8b65aa5160bbd1c67e30ea910 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 4 Jul 2025 11:20:08 +0000 Subject: [PATCH 19/22] Convert IPersistentComponentStateSerializer<T> interface to abstract class PersistentComponentStateSerializer<T> Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../src/IPersistentComponentStateSerializer.cs | 2 +- ...zerOfT.cs => PersistentComponentStateSerializer.cs} | 6 +++--- .../Components/src/PersistentStateValueProvider.cs | 8 ++++---- src/Components/Components/src/PublicAPI.Unshipped.txt | 10 ++++------ .../test/IPersistentComponentStateSerializerTests.cs | 6 +++--- .../RazorComponentEndpointsStartup.cs | 2 +- .../test/testassets/Components.WasmMinimal/Program.cs | 2 +- .../TestContentPackage/CustomIntSerializer.cs | 6 +++--- 8 files changed, 20 insertions(+), 22 deletions(-) rename src/Components/Components/src/{IPersistentComponentStateSerializerOfT.cs => PersistentComponentStateSerializer.cs} (87%) diff --git a/src/Components/Components/src/IPersistentComponentStateSerializer.cs b/src/Components/Components/src/IPersistentComponentStateSerializer.cs index f6146749f24d..9fe18f53ce92 100644 --- a/src/Components/Components/src/IPersistentComponentStateSerializer.cs +++ b/src/Components/Components/src/IPersistentComponentStateSerializer.cs @@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.Components; -public interface IPersistentComponentStateSerializer +internal interface IPersistentComponentStateSerializer { Task PersistAsync(Type type, object value, IBufferWriter writer); object Restore(Type type, ReadOnlySequence data); diff --git a/src/Components/Components/src/IPersistentComponentStateSerializerOfT.cs b/src/Components/Components/src/PersistentComponentStateSerializer.cs similarity index 87% rename from src/Components/Components/src/IPersistentComponentStateSerializerOfT.cs rename to src/Components/Components/src/PersistentComponentStateSerializer.cs index 5e3c311989e6..798a177b998c 100644 --- a/src/Components/Components/src/IPersistentComponentStateSerializerOfT.cs +++ b/src/Components/Components/src/PersistentComponentStateSerializer.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Components; /// Provides custom serialization logic for persistent component state values of type . /// /// The type of the value to serialize. -public interface IPersistentComponentStateSerializer : IPersistentComponentStateSerializer +public abstract class PersistentComponentStateSerializer : IPersistentComponentStateSerializer { /// /// Serializes the provided and writes it to the . @@ -17,7 +17,7 @@ public interface IPersistentComponentStateSerializer : IPersistentComponentSt /// The value to serialize. /// The buffer writer to write the serialized data to. /// A task that represents the asynchronous serialization operation. - Task PersistAsync(T value, IBufferWriter writer); + public abstract Task PersistAsync(T value, IBufferWriter writer); /// /// Deserializes a value of type from the provided . @@ -25,7 +25,7 @@ public interface IPersistentComponentStateSerializer : IPersistentComponentSt /// /// The serialized data to deserialize. /// The deserialized value. - T Restore(ReadOnlySequence data); + public abstract T Restore(ReadOnlySequence data); /// /// Explicit interface implementation for non-generic serialization. diff --git a/src/Components/Components/src/PersistentStateValueProvider.cs b/src/Components/Components/src/PersistentStateValueProvider.cs index a7f59028e2a7..321d161e2d80 100644 --- a/src/Components/Components/src/PersistentStateValueProvider.cs +++ b/src/Components/Components/src/PersistentStateValueProvider.cs @@ -106,10 +106,10 @@ private static PropertyGetter ResolvePropertyGetter(Type type, string propertyNa private IPersistentComponentStateSerializer? SerializerFactory(Type type) { - var serializerType = typeof(IPersistentComponentStateSerializer<>).MakeGenericType(type); + var serializerType = typeof(PersistentComponentStateSerializer<>).MakeGenericType(type); var serializer = serviceProvider.GetService(serializerType); - // The generic interface now inherits from the internal interface, so we can cast directly + // The generic class now inherits from the internal interface, so we can cast directly return serializer as IPersistentComponentStateSerializer; } @@ -331,7 +331,7 @@ private static bool IsSerializableKey(object key) /// The key to use to persist the state. /// The instance to persist. /// The custom serializer to use for serialization. - internal async Task PersistAsync(string key, TValue instance, IPersistentComponentStateSerializer serializer) + internal async Task PersistAsync(string key, TValue instance, PersistentComponentStateSerializer serializer) { ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(serializer); @@ -351,7 +351,7 @@ internal async Task PersistAsync(string key, TValue instance, IPersisten /// The custom serializer to use for deserialization. /// The persisted instance. /// true if the state was found; false otherwise. - internal bool TryTake(string key, IPersistentComponentStateSerializer serializer, [MaybeNullWhen(false)] out TValue instance) + internal bool TryTake(string key, PersistentComponentStateSerializer serializer, [MaybeNullWhen(false)] out TValue instance) { ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(serializer); diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index cb3a8aaa4e32..96033381f42c 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -16,12 +16,10 @@ Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateS Microsoft.AspNetCore.Components.PersistentStateAttribute Microsoft.AspNetCore.Components.PersistentStateAttribute.PersistentStateAttribute() -> void Microsoft.AspNetCore.Components.Infrastructure.PersistentStateProviderServiceCollectionExtensions -Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer -Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.PersistAsync(System.Type! type, object! value, System.Buffers.IBufferWriter! writer) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.Restore(System.Type! type, System.Buffers.ReadOnlySequence data) -> object! -Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer -Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.PersistAsync(T value, System.Buffers.IBufferWriter! writer) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Components.IPersistentComponentStateSerializer.Restore(System.Buffers.ReadOnlySequence data) -> T +Microsoft.AspNetCore.Components.PersistentComponentStateSerializer +Microsoft.AspNetCore.Components.PersistentComponentStateSerializer.PersistentComponentStateSerializer() -> void +abstract Microsoft.AspNetCore.Components.PersistentComponentStateSerializer.PersistAsync(T value, System.Buffers.IBufferWriter! writer) -> System.Threading.Tasks.Task! +abstract Microsoft.AspNetCore.Components.PersistentComponentStateSerializer.Restore(System.Buffers.ReadOnlySequence data) -> T static Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.AspNetCore.Components.IComponentRenderMode! componentRenderMode) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsMetrics(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs b/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs index fac78895b623..282cd9b512b8 100644 --- a/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs +++ b/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs @@ -62,16 +62,16 @@ public void TryTake_CanUseCustomSerializer() Assert.Equal(customData, retrievedValue); } - private class TestStringSerializer : IPersistentComponentStateSerializer + private class TestStringSerializer : PersistentComponentStateSerializer { - public Task PersistAsync(string value, IBufferWriter writer) + public override Task PersistAsync(string value, IBufferWriter writer) { var bytes = Encoding.UTF8.GetBytes(value); writer.Write(bytes); return Task.CompletedTask; } - public string Restore(ReadOnlySequence data) + public override string Restore(ReadOnlySequence data) { var bytes = data.ToArray(); return Encoding.UTF8.GetString(bytes); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index ca9b936766f5..6ff528a5a0eb 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -67,7 +67,7 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); // Register custom serializer for E2E testing of persistent component state serialization extensibility - services.AddSingleton, CustomIntSerializer>(); + services.AddSingleton, CustomIntSerializer>(); services.AddHttpContextAccessor(); services.AddSingleton(); diff --git a/src/Components/test/testassets/Components.WasmMinimal/Program.cs b/src/Components/test/testassets/Components.WasmMinimal/Program.cs index ab9098babd5b..57f0689b6e44 100644 --- a/src/Components/test/testassets/Components.WasmMinimal/Program.cs +++ b/src/Components/test/testassets/Components.WasmMinimal/Program.cs @@ -17,7 +17,7 @@ builder.Services.AddSingleton(); // Register custom serializer for persistent component state -builder.Services.AddSingleton, CustomIntSerializer>(); +builder.Services.AddSingleton, CustomIntSerializer>(); builder.Services.AddCascadingAuthenticationState(); diff --git a/src/Components/test/testassets/TestContentPackage/CustomIntSerializer.cs b/src/Components/test/testassets/TestContentPackage/CustomIntSerializer.cs index a9a65cc8dc7e..8a641b71c1fe 100644 --- a/src/Components/test/testassets/TestContentPackage/CustomIntSerializer.cs +++ b/src/Components/test/testassets/TestContentPackage/CustomIntSerializer.cs @@ -11,9 +11,9 @@ namespace TestContentPackage; /// A custom serializer for int values that uses a custom format to test serialization extensibility. /// This serializer prefixes integer values with "CUSTOM:" to clearly distinguish them from JSON serialization. /// -public class CustomIntSerializer : IPersistentComponentStateSerializer +public class CustomIntSerializer : PersistentComponentStateSerializer { - public Task PersistAsync(int value, IBufferWriter writer) + public override Task PersistAsync(int value, IBufferWriter writer) { var customFormat = $"CUSTOM:{value}"; var bytes = Encoding.UTF8.GetBytes(customFormat); @@ -21,7 +21,7 @@ public Task PersistAsync(int value, IBufferWriter writer) return Task.CompletedTask; } - public int Restore(ReadOnlySequence data) + public override int Restore(ReadOnlySequence data) { var bytes = data.ToArray(); var text = Encoding.UTF8.GetString(bytes); From 0d5d8df7fbb1aaed19c270c501498f613aa76b3a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:00:20 +0000 Subject: [PATCH 20/22] Make PersistAsync method void returning instead of Task returning Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../src/IPersistentComponentStateSerializer.cs | 2 +- .../src/PersistentComponentStateSerializer.cs | 5 ++--- .../Components/src/PersistentStateValueProvider.cs | 13 +++++++------ .../Components/src/PublicAPI.Unshipped.txt | 2 +- .../IPersistentComponentStateSerializerTests.cs | 7 +++---- .../TestContentPackage/CustomIntSerializer.cs | 3 +-- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/Components/Components/src/IPersistentComponentStateSerializer.cs b/src/Components/Components/src/IPersistentComponentStateSerializer.cs index 9fe18f53ce92..4a63590cdd73 100644 --- a/src/Components/Components/src/IPersistentComponentStateSerializer.cs +++ b/src/Components/Components/src/IPersistentComponentStateSerializer.cs @@ -7,6 +7,6 @@ namespace Microsoft.AspNetCore.Components; internal interface IPersistentComponentStateSerializer { - Task PersistAsync(Type type, object value, IBufferWriter writer); + void PersistAsync(Type type, object value, IBufferWriter writer); object Restore(Type type, ReadOnlySequence data); } \ No newline at end of file diff --git a/src/Components/Components/src/PersistentComponentStateSerializer.cs b/src/Components/Components/src/PersistentComponentStateSerializer.cs index 798a177b998c..3b693d488ace 100644 --- a/src/Components/Components/src/PersistentComponentStateSerializer.cs +++ b/src/Components/Components/src/PersistentComponentStateSerializer.cs @@ -16,8 +16,7 @@ public abstract class PersistentComponentStateSerializer : IPersistentCompone /// /// The value to serialize. /// The buffer writer to write the serialized data to. - /// A task that represents the asynchronous serialization operation. - public abstract Task PersistAsync(T value, IBufferWriter writer); + public abstract void PersistAsync(T value, IBufferWriter writer); /// /// Deserializes a value of type from the provided . @@ -30,7 +29,7 @@ public abstract class PersistentComponentStateSerializer : IPersistentCompone /// /// Explicit interface implementation for non-generic serialization. /// - Task IPersistentComponentStateSerializer.PersistAsync(Type type, object value, IBufferWriter writer) + void IPersistentComponentStateSerializer.PersistAsync(Type type, object value, IBufferWriter writer) => PersistAsync((T)value, writer); /// diff --git a/src/Components/Components/src/PersistentStateValueProvider.cs b/src/Components/Components/src/PersistentStateValueProvider.cs index 321d161e2d80..a44ef367161c 100644 --- a/src/Components/Components/src/PersistentStateValueProvider.cs +++ b/src/Components/Components/src/PersistentStateValueProvider.cs @@ -71,26 +71,27 @@ public void Subscribe(ComponentState subscriber, in CascadingParameterInfo param // Resolve serializer outside the lambda var customSerializer = ResolveSerializer(propertyType); - _subscriptions[subscriber] = state.RegisterOnPersisting(async () => + _subscriptions[subscriber] = state.RegisterOnPersisting(() => { var storageKey = ComputeKey(subscriber, propertyName); var propertyGetter = ResolvePropertyGetter(subscriber.Component.GetType(), propertyName); var property = propertyGetter.GetValue(subscriber.Component); if (property == null) { - return; + return Task.CompletedTask; } if (customSerializer != null) { using var writer = new PooledArrayBufferWriter(); - await customSerializer.PersistAsync(propertyType, property, writer); + customSerializer.PersistAsync(propertyType, property, writer); state.PersistAsBytes(storageKey, writer.WrittenMemory.ToArray()); - return; + return Task.CompletedTask; } // Fallback to JSON serialization state.PersistAsJson(storageKey, property, propertyType); + return Task.CompletedTask; }, subscriber.Renderer.GetComponentRenderMode(subscriber.Component)); } @@ -331,13 +332,13 @@ private static bool IsSerializableKey(object key) /// The key to use to persist the state. /// The instance to persist. /// The custom serializer to use for serialization. - internal async Task PersistAsync(string key, TValue instance, PersistentComponentStateSerializer serializer) + internal void PersistAsync(string key, TValue instance, PersistentComponentStateSerializer serializer) { ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(serializer); using var writer = new PooledArrayBufferWriter(); - await serializer.PersistAsync(instance, writer); + serializer.PersistAsync(instance, writer); state.PersistAsBytes(key, writer.WrittenMemory.ToArray()); } diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 96033381f42c..4af4ff3118fb 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -18,7 +18,7 @@ Microsoft.AspNetCore.Components.PersistentStateAttribute.PersistentStateAttribut Microsoft.AspNetCore.Components.Infrastructure.PersistentStateProviderServiceCollectionExtensions Microsoft.AspNetCore.Components.PersistentComponentStateSerializer Microsoft.AspNetCore.Components.PersistentComponentStateSerializer.PersistentComponentStateSerializer() -> void -abstract Microsoft.AspNetCore.Components.PersistentComponentStateSerializer.PersistAsync(T value, System.Buffers.IBufferWriter! writer) -> System.Threading.Tasks.Task! +abstract Microsoft.AspNetCore.Components.PersistentComponentStateSerializer.PersistAsync(T value, System.Buffers.IBufferWriter! writer) -> void abstract Microsoft.AspNetCore.Components.PersistentComponentStateSerializer.Restore(System.Buffers.ReadOnlySequence data) -> T static Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.AspNetCore.Components.IComponentRenderMode! componentRenderMode) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsMetrics(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs b/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs index 282cd9b512b8..e418a7fd1b7a 100644 --- a/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs +++ b/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Components; public class IPersistentComponentStateSerializerTests { [Fact] - public async Task PersistAsync_CanUseCustomSerializer() + public void PersistAsync_CanUseCustomSerializer() { // Arrange var currentState = new Dictionary(); @@ -25,7 +25,7 @@ public async Task PersistAsync_CanUseCustomSerializer() state.PersistingState = true; // Act - await stateValueProvider.PersistAsync("test-key", testValue, customSerializer); + stateValueProvider.PersistAsync("test-key", testValue, customSerializer); // Assert state.PersistingState = false; @@ -64,11 +64,10 @@ public void TryTake_CanUseCustomSerializer() private class TestStringSerializer : PersistentComponentStateSerializer { - public override Task PersistAsync(string value, IBufferWriter writer) + public override void PersistAsync(string value, IBufferWriter writer) { var bytes = Encoding.UTF8.GetBytes(value); writer.Write(bytes); - return Task.CompletedTask; } public override string Restore(ReadOnlySequence data) diff --git a/src/Components/test/testassets/TestContentPackage/CustomIntSerializer.cs b/src/Components/test/testassets/TestContentPackage/CustomIntSerializer.cs index 8a641b71c1fe..60bc3e5bfab3 100644 --- a/src/Components/test/testassets/TestContentPackage/CustomIntSerializer.cs +++ b/src/Components/test/testassets/TestContentPackage/CustomIntSerializer.cs @@ -13,12 +13,11 @@ namespace TestContentPackage; /// public class CustomIntSerializer : PersistentComponentStateSerializer { - public override Task PersistAsync(int value, IBufferWriter writer) + public override void PersistAsync(int value, IBufferWriter writer) { var customFormat = $"CUSTOM:{value}"; var bytes = Encoding.UTF8.GetBytes(customFormat); writer.Write(bytes); - return Task.CompletedTask; } public override int Restore(ReadOnlySequence data) From e9220766779b1c26715902daeb782b18a073a129 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:14:39 +0000 Subject: [PATCH 21/22] Inline ResolveSerializer method call in PersistentStateValueProvider Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../Components/src/PersistentStateValueProvider.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Components/Components/src/PersistentStateValueProvider.cs b/src/Components/Components/src/PersistentStateValueProvider.cs index a44ef367161c..e5da9eb55714 100644 --- a/src/Components/Components/src/PersistentStateValueProvider.cs +++ b/src/Components/Components/src/PersistentStateValueProvider.cs @@ -44,7 +44,7 @@ public bool CanSupplyValue(in CascadingParameterInfo parameterInfo) var storageKey = ComputeKey(componentState, parameterInfo.PropertyName); // Try to get a custom serializer for this type first - var customSerializer = ResolveSerializer(parameterInfo.PropertyType); + var customSerializer = _serializerCache.GetOrAdd(parameterInfo.PropertyType, SerializerFactory); if (customSerializer != null) { @@ -69,7 +69,7 @@ public void Subscribe(ComponentState subscriber, in CascadingParameterInfo param var propertyType = parameterInfo.PropertyType; // Resolve serializer outside the lambda - var customSerializer = ResolveSerializer(propertyType); + var customSerializer = _serializerCache.GetOrAdd(propertyType, SerializerFactory); _subscriptions[subscriber] = state.RegisterOnPersisting(() => { @@ -100,11 +100,6 @@ private static PropertyGetter ResolvePropertyGetter(Type type, string propertyNa return _propertyGetterCache.GetOrAdd((type, propertyName), PropertyGetterFactory); } - private IPersistentComponentStateSerializer? ResolveSerializer(Type type) - { - return _serializerCache.GetOrAdd(type, SerializerFactory); - } - private IPersistentComponentStateSerializer? SerializerFactory(Type type) { var serializerType = typeof(PersistentComponentStateSerializer<>).MakeGenericType(type); From ebe6f529f80c781fa015260b263cddea48cabcd3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:26:20 +0000 Subject: [PATCH 22/22] Drop "Async" suffix from Persist method names since they now return void Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> --- .../Components/src/IPersistentComponentStateSerializer.cs | 2 +- .../Components/src/PersistentComponentStateSerializer.cs | 6 +++--- .../Components/src/PersistentStateValueProvider.cs | 4 ++-- src/Components/Components/src/PublicAPI.Unshipped.txt | 2 +- .../test/IPersistentComponentStateSerializerTests.cs | 2 +- .../testassets/TestContentPackage/CustomIntSerializer.cs | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Components/Components/src/IPersistentComponentStateSerializer.cs b/src/Components/Components/src/IPersistentComponentStateSerializer.cs index 4a63590cdd73..bcbfebbacf0a 100644 --- a/src/Components/Components/src/IPersistentComponentStateSerializer.cs +++ b/src/Components/Components/src/IPersistentComponentStateSerializer.cs @@ -7,6 +7,6 @@ namespace Microsoft.AspNetCore.Components; internal interface IPersistentComponentStateSerializer { - void PersistAsync(Type type, object value, IBufferWriter writer); + void Persist(Type type, object value, IBufferWriter writer); object Restore(Type type, ReadOnlySequence data); } \ No newline at end of file diff --git a/src/Components/Components/src/PersistentComponentStateSerializer.cs b/src/Components/Components/src/PersistentComponentStateSerializer.cs index 3b693d488ace..c3705bdc4197 100644 --- a/src/Components/Components/src/PersistentComponentStateSerializer.cs +++ b/src/Components/Components/src/PersistentComponentStateSerializer.cs @@ -16,7 +16,7 @@ public abstract class PersistentComponentStateSerializer : IPersistentCompone /// /// The value to serialize. /// The buffer writer to write the serialized data to. - public abstract void PersistAsync(T value, IBufferWriter writer); + public abstract void Persist(T value, IBufferWriter writer); /// /// Deserializes a value of type from the provided . @@ -29,8 +29,8 @@ public abstract class PersistentComponentStateSerializer : IPersistentCompone /// /// Explicit interface implementation for non-generic serialization. /// - void IPersistentComponentStateSerializer.PersistAsync(Type type, object value, IBufferWriter writer) - => PersistAsync((T)value, writer); + void IPersistentComponentStateSerializer.Persist(Type type, object value, IBufferWriter writer) + => Persist((T)value, writer); /// /// Explicit interface implementation for non-generic deserialization. diff --git a/src/Components/Components/src/PersistentStateValueProvider.cs b/src/Components/Components/src/PersistentStateValueProvider.cs index e5da9eb55714..669e0b5a5363 100644 --- a/src/Components/Components/src/PersistentStateValueProvider.cs +++ b/src/Components/Components/src/PersistentStateValueProvider.cs @@ -84,7 +84,7 @@ public void Subscribe(ComponentState subscriber, in CascadingParameterInfo param if (customSerializer != null) { using var writer = new PooledArrayBufferWriter(); - customSerializer.PersistAsync(propertyType, property, writer); + customSerializer.Persist(propertyType, property, writer); state.PersistAsBytes(storageKey, writer.WrittenMemory.ToArray()); return Task.CompletedTask; } @@ -333,7 +333,7 @@ internal void PersistAsync(string key, TValue instance, PersistentCompon ArgumentNullException.ThrowIfNull(serializer); using var writer = new PooledArrayBufferWriter(); - serializer.PersistAsync(instance, writer); + serializer.Persist(instance, writer); state.PersistAsBytes(key, writer.WrittenMemory.ToArray()); } diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 4af4ff3118fb..311fe70d9848 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -18,7 +18,7 @@ Microsoft.AspNetCore.Components.PersistentStateAttribute.PersistentStateAttribut Microsoft.AspNetCore.Components.Infrastructure.PersistentStateProviderServiceCollectionExtensions Microsoft.AspNetCore.Components.PersistentComponentStateSerializer Microsoft.AspNetCore.Components.PersistentComponentStateSerializer.PersistentComponentStateSerializer() -> void -abstract Microsoft.AspNetCore.Components.PersistentComponentStateSerializer.PersistAsync(T value, System.Buffers.IBufferWriter! writer) -> void +abstract Microsoft.AspNetCore.Components.PersistentComponentStateSerializer.Persist(T value, System.Buffers.IBufferWriter! writer) -> void abstract Microsoft.AspNetCore.Components.PersistentComponentStateSerializer.Restore(System.Buffers.ReadOnlySequence data) -> T static Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.AspNetCore.Components.IComponentRenderMode! componentRenderMode) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsMetrics(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs b/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs index e418a7fd1b7a..2b237fb2431b 100644 --- a/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs +++ b/src/Components/Components/test/IPersistentComponentStateSerializerTests.cs @@ -64,7 +64,7 @@ public void TryTake_CanUseCustomSerializer() private class TestStringSerializer : PersistentComponentStateSerializer { - public override void PersistAsync(string value, IBufferWriter writer) + public override void Persist(string value, IBufferWriter writer) { var bytes = Encoding.UTF8.GetBytes(value); writer.Write(bytes); diff --git a/src/Components/test/testassets/TestContentPackage/CustomIntSerializer.cs b/src/Components/test/testassets/TestContentPackage/CustomIntSerializer.cs index 60bc3e5bfab3..487a7b2de45e 100644 --- a/src/Components/test/testassets/TestContentPackage/CustomIntSerializer.cs +++ b/src/Components/test/testassets/TestContentPackage/CustomIntSerializer.cs @@ -13,7 +13,7 @@ namespace TestContentPackage; /// public class CustomIntSerializer : PersistentComponentStateSerializer { - public override void PersistAsync(int value, IBufferWriter writer) + public override void Persist(int value, IBufferWriter writer) { var customFormat = $"CUSTOM:{value}"; var bytes = Encoding.UTF8.GetBytes(customFormat);