dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.35k stars 9.99k forks source link

Blazor Server PersistComponentState public Persist/TryTake from byte[] methods #45574

Open TheAtomicOption opened 1 year ago

TheAtomicOption commented 1 year ago

Background and Motivation

Currently for Blazor Server with prerendering, component state can only be persisted with PersistAsJson() and retrieved with TryTakeFromJson<T>(). So while the underlying implementation stores this JSON in a Dictionary<string, byte[]>, it's not possible to directly serialize objects as byte[].

The performance and inability to pass JsonSerializerOptions (e.g. https://github.com/dotnet/aspnetcore/issues/44280) has been raised before. Serializing with custom options like IncludeFields is only possible by serializing twice. First to a json string and then to PersistAsJson() where that string is serialized again.

Proposed API

Allow serialization directly to byte[]. This would make it possible to use faster or more customized serialization libraries like MemoryPack when appropriate while also taking up less server memory, similar to what was done for JSInterop serialization with the release of .NET6.

example:

namespace Microsoft.AspNetCore.Components;

public class PersistentComponentState
{ 

- private bool TryTake(string key, out byte[]? value)
+ public bool TryTake(string key, out byte[]? value)

+ public void PersistByteArray(string key, byte[] serializedObject){
+    if (key is null)
+    {
+        throw new ArgumentNullException(nameof(key));
+    }
+    if (!PersistingState)
+    {
+        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}'");
+    }
+    _currentState.Add(key, serializedObject);
+ }

}

Usage Examples

Usage would be similar to existing component persistence except the user would have to provide the serializer.


@implements IDisposable
@inject PersistentComponentState ApplicationState
@inject ICustomByteSerializationSingleton customSerializer
...

@code {
    private {TYPE} data;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        persistingSubscription = 
            ApplicationState.RegisterOnPersisting(PersistData);

        if (ApplicationState.TryTake("{TOKEN}", out var restored))
        {
            data = customSerializer.Deserialize<{TYPE}>(restored);
        }
        else {
            data = await ...;
        }

    }

    private Task PersistData()
    {
        byte[] serializedObject = customSerializer.Serializer(data);
        ApplicationState.PersistByteArray("{TOKEN}", serializerdObject);
        return Task.CompletedTask;
    }

    void IDisposable.Dispose()
    {
        persistingSubscription.Dispose();
    }
}

Alternatives

An alternative might be to offer a way to provide an entirely custom PersistComponentState implementation in Startup.cs configuration via an interface. That would be more complex for users to implement, but would offer similar amounts of freedom and could allow use of things like a Redis cache to make component state restorable across a server farm.

Risks

The main risk here is that someone attempts to serialize things improperly and ends up trying to deserialize bad references, or that the underlying Dictionary<string, byte[]> implementation is replaced with something else that can't easily support serialization to byte[].

javiercn commented 1 year ago

@TheAtomicOption thanks for contacting us.

We were going to initially offer this functionality, but we cut it because were late on the release cycle and did not want to introduce risk.

That said, we do not want to offer byte[] based APIs but APIs based on IBufferWritter<byte> and ReadOnlySequence<byte> as we want to be able to manage the underlying buffers ourselves.

ghost commented 1 year ago

Thanks for contacting us.

We're moving this issue to the .NET 8 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

ghost commented 1 year ago

Thanks for contacting us.

We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

ghost commented 10 months ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.