Description
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]