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

Merged
merged 22 commits into from
Jul 7, 2025

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
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
…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
Copilot finished work on behalf of javiercn July 4, 2025 11:05
…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

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
{
ArgumentNullException.ThrowIfNull(key);

if (!PersistingState)
Copy link
Member

Choose a reason for hiding this comment

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

These code with checks is shared with PersistAsJson.

internal void PersistAsBytes(string key, byte[] data)
{
   if (PersistanceAllowed(key))
   {
      _currentState.Add(key, data);
   }
}

@javiercn javiercn enabled auto-merge (squash) July 7, 2025 16:00
@javiercn
Copy link
Member

javiercn commented Jul 7, 2025

/ba-g Unrelated build failures

@javiercn javiercn merged commit 8883b98 into main Jul 7, 2025
27 of 29 checks passed
@javiercn javiercn deleted the copilot/fix-60716 branch July 7, 2025 16:01
@dotnet-policy-service dotnet-policy-service bot added this to the 10.0-preview7 milestone Jul 7, 2025
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
3 participants