-
Notifications
You must be signed in to change notification settings - Fork 10.4k
[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
[Blazor] Add IPersistentComponentStateSerializer<T> interface for custom serialization extensibility #62559
Changes from 3 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
e2d0602
Initial plan
Copilot 726d6f5
Initial planning for persistent component state serialization extensi…
Copilot efb1c3b
Implement IPersistentComponentStateSerializer interface and core func…
Copilot 6f2f33c
Add comprehensive tests and finalize serialization extensibility impl…
Copilot 147b5bc
Make PersistAsync and TryTake methods internal for custom serializers
Copilot d7ed4d9
Implement interface architecture changes for custom serializers per f…
Copilot fc6a4d9
Fix serializer cache to be instance-based to prevent cross-test conta…
Copilot 358f9e4
Make IPersistentComponentStateSerializer interface internal per code …
Copilot 4c3dcac
Make IPersistentComponentStateSerializer<T> public while keepin…
Copilot df6eb65
Move PersistAsync and TryTake methods to PersistentStateValueProvider…
Copilot cc5109e
Add custom serializer support to E2E tests for persistent component s…
Copilot 2085568
Fix CA1310 analyzer error by specifying StringComparison.Ordinal in C…
Copilot b6723a6
Move CustomIntSerializer to shared TestContentPackage and register in…
Copilot 67ca5ec
Register CustomIntSerializer in Components.WasmMinimal for E2E test c…
Copilot c1fefc5
Make IPersistentComponentStateSerializer internal and move generic in…
Copilot cbf9fa9
Implement explicit interface implementation for custom serializers an…
Copilot 365064d
Implement explicit interface implementation in IPersistentComponentSt…
Copilot 3b3af1d
Remove redundant CustomIntSerializer registration from BasicTestApp
Copilot 970977b
Convert IPersistentComponentStateSerializer<T> interface to abs…
Copilot 0d5d8df
Make PersistAsync method void returning instead of Task returning
Copilot e922076
Inline ResolveSerializer method call in PersistentStateValueProvider
Copilot ebe6f52
Drop "Async" suffix from Persist method names since they now return void
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Empty file.
30 changes: 30 additions & 0 deletions
30
src/Components/Components/src/IPersistentComponentStateSerializer.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
/// <summary> | ||
/// 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> | ||
{ | ||
/// <summary> | ||
/// Serializes the provided <paramref name="value"/> and writes it to the <paramref name="writer"/>. | ||
/// </summary> | ||
/// <param name="value">The value to serialize.</param> | ||
/// <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); | ||
|
||
/// <summary> | ||
/// Deserializes a value of type <typeparamref name="T"/> from the provided <paramref name="data"/>. | ||
/// This method must be synchronous to avoid UI tearing during component state restoration. | ||
/// </summary> | ||
/// <param name="data">The serialized data to deserialize.</param> | ||
/// <returns>The deserialized value.</returns> | ||
T Restore(ReadOnlySequence<byte> data); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)); | ||
} | ||
|
||
/// <summary> | ||
/// Serializes <paramref name="instance"/> using the provided <paramref name="serializer"/> and persists it under the given <paramref name="key"/>. | ||
/// </summary> | ||
/// <typeparam name="TValue">The <paramref name="instance"/> type.</typeparam> | ||
/// <param name="key">The key to use to persist the state.</param> | ||
/// <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) | ||
javiercn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
ArgumentNullException.ThrowIfNull(key); | ||
ArgumentNullException.ThrowIfNull(serializer); | ||
|
||
if (!PersistingState) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These code with checks is shared with
|
||
{ | ||
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<byte>(); | ||
await serializer.PersistAsync(instance, writer, cancellationToken); | ||
_currentState.Add(key, writer.WrittenMemory.ToArray()); | ||
} | ||
|
||
/// <summary> | ||
/// Tries to retrieve the persisted state as JSON with the given <paramref name="key"/> and deserializes it into an | ||
/// instance of type <typeparamref name="TValue"/>. | ||
|
@@ -155,6 +184,34 @@ internal bool TryTakeFromJson(string key, [DynamicallyAccessedMembers(JsonSerial | |
} | ||
} | ||
|
||
/// <summary> | ||
/// Tries to retrieve the persisted state with the given <paramref name="key"/> and deserializes it using the provided <paramref name="serializer"/> into an | ||
/// instance of type <typeparamref name="TValue"/>. | ||
/// When the key is present, the state is successfully returned via <paramref name="instance"/> | ||
/// and removed from the <see cref="PersistentComponentState"/>. | ||
/// </summary> | ||
/// <param name="key">The key used to persist the instance.</param> | ||
/// <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) | ||
javiercn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
ArgumentNullException.ThrowIfNull(key); | ||
ArgumentNullException.ThrowIfNull(serializer); | ||
|
||
if (TryTake(key, out var data)) | ||
{ | ||
var sequence = new ReadOnlySequence<byte>(data!); | ||
instance = serializer.Restore(sequence); | ||
return true; | ||
} | ||
else | ||
{ | ||
instance = default; | ||
return false; | ||
} | ||
} | ||
|
||
private bool TryTake(string key, out byte[]? value) | ||
{ | ||
ArgumentNullException.ThrowIfNull(key); | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
73 changes: 73 additions & 0 deletions
73
src/Components/Components/test/IPersistentComponentStateSerializerTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string, byte[]>(); | ||
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<string, byte[]>(), []); | ||
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<string, byte[]> { { "test-key", customBytes } }; | ||
|
||
var state = new PersistentComponentState(new Dictionary<string, byte[]>(), []); | ||
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<string> | ||
{ | ||
public Task PersistAsync(string value, IBufferWriter<byte> writer, CancellationToken cancellationToken) | ||
{ | ||
var bytes = Encoding.UTF8.GetBytes(value); | ||
writer.Write(bytes); | ||
return Task.CompletedTask; | ||
} | ||
|
||
public string Restore(ReadOnlySequence<byte> data) | ||
{ | ||
var bytes = data.ToArray(); | ||
return Encoding.UTF8.GetString(bytes); | ||
} | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.