Skip to content

[Blazor] PersistentComponentStateSerializer API review #62589

Open
@javiercn

Description

@javiercn

Background and Motivation

Currently, Blazor's persistent component state feature only supports JSON serialization through the PersistentStateAttribute. This limitation forces developers to either accept the default JSON serialization behavior or avoid using the declarative persistent state feature entirely when they need custom serialization logic.

This API enables developers to provide custom serialization logic for specific types when using the [PersistentState] attribute on component properties, making the declarative persistent state feature more flexible and broadly applicable.

Original issue: #60716
Implementation PR: #62559 (commit 8883b98)

Proposed API

namespace Microsoft.AspNetCore.Components;

+ public abstract class PersistentComponentStateSerializer<T>
+ {
+     public abstract void Persist(T value, IBufferWriter<byte> writer);
+     public abstract T Restore(ReadOnlySequence<byte> data);
+ }

Usage Examples

Basic Usage - Custom Binary Serialization with Protobuf

// Define a custom serializer using protobuf-net
public class ProtobufSerializer<T> : PersistentComponentStateSerializer<T>
{
    public override void Persist(T value, IBufferWriter<byte> writer)
    {
        Serializer.Serialize(new BufferWriterStream(writer), value);
    }

    public override T Restore(ReadOnlySequence<byte> data)
    {
        return Serializer.Deserialize<T>(new ReadOnlySequenceStream(data));
    }
}

// Register the serializer in Program.cs
builder.Services.AddSingleton<PersistentComponentStateSerializer<UserData>, ProtobufSerializer<UserData>>();

// Use in a component - works automatically with custom serializer
@page "/user-profile"

<h3>User Profile</h3>

@code {
    [PersistentState]
    public UserData CurrentUser { get; set; } = new();
    
    protected override void OnInitialized()
    {
        // State is automatically restored using custom serializer
        CurrentUser ??= new UserData { Id = 1, Name = "Guest" };
    }
}

Alternative Designs

1. Interface-based approach (original proposal)

The original issue proposed an interface IPersistentComponentStateSerializer with non-generic methods:

public interface IPersistentComponentStateSerializer
{
    Task Persist(Type type, object instance, IBufferWriter buffer);
    void Restore(Type type, ReadOnlySequence<byte> data);
}

Why we chose the abstract class approach:

  • Type safety through generics eliminates casting
  • Cleaner API for implementers - they work directly with their type
  • Better IntelliSense and compile-time checking
  • Follows patterns established by other serialization APIs in .NET

2. Attribute-based serializer specification

We considered allowing serializer specification directly on the property:

[PersistentState(Serializer = typeof(CustomSerializer))]
public MyData Data { get; set; }

Why we chose DI-based approach:

  • Allows serializers to have dependencies (like IDataProtector)
  • Single registration point for each type
  • Consistent with ASP.NET Core's DI-first philosophy
  • Easier to test and mock

Why we chose synchronous methods:

  • Avoids complexity in the persistence pipeline
  • Matches the synchronous nature of component rendering
  • Restore must be synchronous anyway to avoid UI tearing

Risks

Performance Considerations

  • Custom serializers could introduce performance bottlenecks if not implemented efficiently
  • No built-in protection against serializers that allocate excessively
  • Mitigation: Documentation emphasizes using IBufferWriter<byte> efficiently

Compatibility

  • The feature gracefully falls back to JSON serialization when no custom serializer is registered
  • No breaking changes to existing code using [PersistentState]

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-blazorIncludes: Blazor, Razor Components

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions