Skip to content

[Blazor] Add IPersistentComponentStateSerializer<T> interface for custom serialization extensibility #62559

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: main
Choose a base branch
from

Conversation

Copilot
Copy link
Contributor

@Copilot Copilot AI commented Jul 3, 2025

This PR implements serialization extensibility for declarative persistent component state, allowing developers to provide custom serializers beyond the built-in JSON serialization.

Changes

New Interface

public interface IPersistentComponentStateSerializer<T>
{
    Task PersistAsync(T value, IBufferWriter<byte> writer, CancellationToken cancellationToken);
    T Restore(ReadOnlySequence<byte> data);
}

Key Features

  • Async Persist / Sync Restore: PersistAsync is async for serialization work, while Restore is synchronous to prevent UI tearing
  • Memory Efficient: Uses IBufferWriter<byte> with PooledArrayBufferWriter to minimize allocations, avoiding byte[] APIs
  • DI Integration: Custom serializers can be registered via dependency injection
  • Automatic Fallback: Falls back to existing JSON serialization when no custom serializer is registered
  • Full Backward Compatibility: All existing persistent state APIs continue to work unchanged

Usage Example

Register custom serializers in DI:

// Program.cs
builder.Services.AddSingleton<IPersistentComponentStateSerializer<User>, CustomUserSerializer>();

Components work exactly as before:

public class UserComponent : ComponentBase 
{
    [PersistentState] 
    public User CurrentUser { get; set; } = new();
    // User will be automatically persisted/restored with custom serializer
}

Implementation Details

  • PersistentComponentState: Extended with new PersistAsync<T> and TryTake<T> methods for custom serializers
  • PersistentStateValueProvider: Updated to attempt DI resolution of custom serializers before falling back to JSON
  • Type Safety: Strongly typed interface ensures compile-time safety for serializer implementations

Testing

  • Added comprehensive tests for custom serializer functionality
  • All 35 existing tests continue to pass, ensuring no breaking changes
  • Validates both custom serializer usage and JSON fallback scenarios

This enables scenarios like compression, encryption, or optimized binary formats for specific types while maintaining the simple declarative syntax of [PersistentState].

Fixes #60716.


💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click here to start the survey.

Copilot AI and others added 3 commits July 3, 2025 16:01
…bility

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
…tionality

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
…ementation

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
@Copilot Copilot AI changed the title [WIP] [Blazor] Support serialization extensibility for declarative persistent component state [Blazor] Add IPersistentComponentStateSerializer<T> interface for custom serialization extensibility Jul 3, 2025
@Copilot Copilot AI requested a review from javiercn July 3, 2025 16:15
Copilot finished work on behalf of javiercn July 3, 2025 16:15
/// <param name="instance">The instance to persist.</param>
/// <param name="serializer">The custom serializer to use for serialization.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the serialization operation.</param>
public async Task PersistAsync<TValue>(string key, TValue instance, IPersistentComponentStateSerializer<TValue> serializer, CancellationToken cancellationToken = default)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this internal

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made this method internal as requested, along with the corresponding TryTake method for consistency. Updated reflection calls in PersistentStateValueProvider to use NonPublic binding flags. Commit 147b5bc

/// <param name="writer">The buffer writer to write the serialized data to.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the serialization operation.</param>
/// <returns>A task that represents the asynchronous serialization operation.</returns>
Task PersistAsync(T value, IBufferWriter<byte> writer, CancellationToken cancellationToken);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No cancellation token

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the cancellation token parameter from the PersistAsync method as requested. Updated both the interface definition and all related implementations. Commit d7ed4d9

/// <param name="serializer">The custom serializer to use for deserialization.</param>
/// <param name="instance">The persisted instance.</param>
/// <returns><c>true</c> if the state was found; <c>false</c> otherwise.</returns>
public bool TryTake<TValue>(string key, IPersistentComponentStateSerializer<TValue> serializer, [MaybeNullWhen(false)] out TValue instance)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this API internal

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made the TryTake API internal as requested. This was already done in the previous commit but confirmed it's correct. The method is now properly scoped to internal access only. Commit d7ed4d9

Comment on lines 83 to 96
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;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cache the serializers by type on a ConcurrentDictionary<Type, IPersistentComponentStateSerializer> (see how it's done in this file.

IPersistentComponentStateSerializer should be an internal interface, have the same methods as IPersistentComponentStateSerializer but take the Type type argument as a first argument.

IPersistentComponentStateSerializer<T> should implement IPersistentComponentStateSerializer with a default interface implementation that calls into Persist no matter the Type t passed as first argument

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this code outside of the lambda.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented the requested architecture changes:

  • Added ConcurrentDictionary<Type, IPersistentComponentStateSerializer> for serializer caching (made instance-based to avoid cross-test contamination)
  • Created internal IPersistentComponentStateSerializer base interface with Type parameter
  • Made IPersistentComponentStateSerializer implement the base interface with default implementations
  • Moved serializer resolution code outside of lambda for better performance
  • Used non-generic interface methods to avoid reflection

Commit d7ed4d9

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
@Copilot Copilot AI requested a review from javiercn July 3, 2025 16:27
Copilot finished work on behalf of javiercn July 3, 2025 16:27
Copilot AI and others added 2 commits July 3, 2025 16:43
…eedback

- 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>
…mination

- 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>
Copilot finished work on behalf of javiercn July 3, 2025 16:52
/// <summary>
/// Provides custom serialization logic for persistent component state values.
/// </summary>
public interface IPersistentComponentStateSerializer
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this interface internal

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made the IPersistentComponentStateSerializer interface internal as requested. Also updated the generic interface IPersistentComponentStateSerializer to internal to maintain consistent accessibility and removed the interfaces from PublicAPI.Unshipped.txt since they are no longer part of the public API. Commit 358f9e4

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot

No. IPersistentComponentStateSerializer should be internal, and IPersistentComponentStateSerializer<T> should be public. Also move it to a separate file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made IPersistentComponentStateSerializer internal and moved IPersistentComponentStateSerializer to a separate file (IPersistentComponentStateSerializerOfT.cs) while keeping it public. Added adapter pattern in PersistentStateValueProvider to bridge between the interfaces. Updated PublicAPI.Unshipped.txt accordingly. Commit c1fefc5

…review feedback

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
@Copilot Copilot AI requested a review from javiercn July 3, 2025 17:19
Copilot finished work on behalf of javiercn July 3, 2025 17:19
/// Provides custom serialization logic for persistent component state values of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of the value to serialize.</typeparam>
internal interface IPersistentComponentStateSerializer<T> : IPersistentComponentStateSerializer
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IPersistentComponentStateSerializer should be public, but IPersistentComponentStateSerializer should remain internal

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made IPersistentComponentStateSerializer<T> public while keeping the base interface IPersistentComponentStateSerializer internal as requested. Also added the public interface to PublicAPI.Unshipped.txt. Commit 4c3dcac

@Copilot Copilot AI requested a review from javiercn July 4, 2025 10:36
Copilot finished work on behalf of javiercn July 4, 2025 10:36
Comment on lines 49 to 50
// Register custom serializer for E2E testing of persistent component state serialization extensibility
builder.Services.AddSingleton<IPersistentComponentStateSerializer<int>, CustomIntSerializer>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this? Since it's already on WasmMinimal

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're correct - the registration is not needed since E2E tests use Components.WasmMinimal for WebAssembly rendering, not BasicTestApp. Removed the redundant registration and unused using directive. Commit 3b3af1d

…ateSerializer&lt;T&gt; and make base interface public

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
@Copilot Copilot AI requested a review from javiercn July 4, 2025 10:53
Copilot finished work on behalf of javiercn July 4, 2025 10:53
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Copilot finished work on behalf of javiercn July 4, 2025 11:05
/// Provides custom serialization logic for persistent component state values of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of the value to serialize.</typeparam>
public interface IPersistentComponentStateSerializer<T> : IPersistentComponentStateSerializer
Copy link
Member

@javiercn javiercn Jul 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this a class PersistentComponentStateSerializer<T> and make the interface internal

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converted IPersistentComponentStateSerializer<T> interface to abstract class PersistentComponentStateSerializer<T> and made the base interface internal as requested. Updated all implementations, tests, and service registrations accordingly. Commit 970977b

…tract class PersistentComponentStateSerializer&lt;T&gt;

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Copilot finished work on behalf of javiercn July 4, 2025 11:20
@Copilot Copilot AI requested a review from javiercn July 4, 2025 11:20
/// <param name="value">The value to serialize.</param>
/// <param name="writer">The buffer writer to write the serialized data to.</param>
/// <returns>A task that represents the asynchronous serialization operation.</returns>
public abstract Task PersistAsync(T value, IBufferWriter<byte> writer);
Copy link
Member

@javiercn javiercn Jul 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this method void returning and drop the Async suffix

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made the PersistAsync method void returning as requested. Updated the interface, abstract class, all implementations, and test methods. All 45 persistent state tests continue to pass. Commit 0d5d8df


internal interface IPersistentComponentStateSerializer
{
Task PersistAsync(Type type, object value, IBufferWriter<byte> writer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop the task and the async suffix (return void)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interface method is already void returning as requested. This was addressed in a previous commit (0d5d8df).

Comment on lines 102 to 105
private IPersistentComponentStateSerializer? ResolveSerializer(Type type)
{
return _serializerCache.GetOrAdd(type, SerializerFactory);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline this method call

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inlined the ResolveSerializer method call at both usage sites and removed the method definition. Commit e922076

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
@Copilot Copilot AI requested a review from javiercn July 4, 2025 14:01
Copilot finished work on behalf of javiercn July 4, 2025 14:01
/// </summary>
/// <param name="value">The value to serialize.</param>
/// <param name="writer">The buffer writer to write the serialized data to.</param>
public abstract void PersistAsync(T value, IBufferWriter<byte> writer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop the async suffix from the method name here and on the base interface

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped the "Async" suffix from both the interface and abstract class method names since they now return void instead of Task. Updated all implementations and tests accordingly. Commit ebe6f52

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
@Copilot Copilot AI requested a review from javiercn July 4, 2025 14:15
Copilot finished work on behalf of javiercn July 4, 2025 14:15
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Copilot finished work on behalf of javiercn July 4, 2025 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Blazor] Support serialization extensibility for declarative persistent component state
2 participants