graphql-platform icon indicating copy to clipboard operation
graphql-platform copied to clipboard

Make StrawberryShake play nicely with Blazor's PersistentComponentState

Open nloum opened this issue 8 months ago • 0 comments

Product

Strawberry Shake

Is your feature request related to a problem?

TL;DR: Yes. StrawberryShake almost works with Blazor's PersistentComponentState. The only thing missing is that you can't serialize the result from an ExecuteAsync call to and from JSON.

Long explanation:

I have a Blazor app that is using interactive auto mode. That means that each component is rendered once on the server, then sent over the wire to the client where it is instantly displayed, then once the WebAssembly files transfer over they take over the component and make it interactive. Ideally you'll have application state that comes from rendering the component on the server, that application state is transferred over the wire to the client, and the client can access that state afterwards without having to do the query again.

I'd like to be able to do an in-memory GraphQL query on the server when pre-rendering, transfer the result of the query over the wire to the client, then when I'm running in wasm, reuse that data and also watch the query. It almost works...but there is one showstopper and one minor annoyance.

The showstopper is that you can't persist interfaces as JSON. Basically I need to convert the result of a GraphQL operation to JSON, but the result from MyOperation.ExecuteAsync() is all interfaces.

The second issue is mostly just annoying. Basically I have to generate the StrawberryShake client once using two transport profiles, one is in-memory for use on the server and the other is http / websocket for use on the client. This is great except I have to include both transport setups in the blazor code. I can't generate two separate StrawberryShake clients (one in the blazor wasm project and one in the aspnetcore project) because that would cause there to be two separate IMyClient interfaces, and injecting that into the blazor component wouldn't work--I have to pick one to inject.

Here's an example component that shows the error I'm getting. I can put together a full demo project if that's needed.

@inject ILaunchMyAppClient Client
@implements IDisposable
@inject PersistentComponentState ApplicationState
@rendermode InteractiveAuto

<h3>Me</h3>

@if (Result is not null)
{
    <p>Name: @Result.Name</p>
}

@code {
    IDisposable? _subscription;
    PersistingComponentStateSubscription persistingSubscription;
    private bool _isOnClient;

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

        // The following line throws an exception like this:
//         Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
//       Unhandled exception rendering component: Deserialization of interface types is not supported. Type 'LaunchMyApp.Client.IMe_Me'. Path: $ | LineNumber: 0 | BytePositionInLine: 1.
// System.NotSupportedException: Deserialization of interface types is not supported. Type 'LaunchMyApp.Client.IMe_Me'. Path: $ | LineNumber: 0 | BytePositionInLine: 1.
//  ---> System.NotSupportedException: Deserialization of interface types is not supported. Type 'LaunchMyApp.Client.IMe_Me'.
//    --- End of inner exception stack trace ---
//    at System.Text.Json.ThrowHelper.ThrowNotSupportedException(ReadStack& state, Utf8JsonReader& reader, NotSupportedException ex)
//    at System.Text.Json.ThrowHelper.ThrowNotSupportedException_DeserializeNoConstructor(Type type, Utf8JsonReader& reader, ReadStack& state)
//    at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1[[LaunchMyApp.Client.IMe_Me, LaunchMyApp.Client, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, IMe_Me& value)
//    at System.Text.Json.Serialization.JsonConverter`1[[LaunchMyApp.Client.IMe_Me, LaunchMyApp.Client, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, IMe_Me& value, Boolean& isPopulatedValue)
//    at System.Text.Json.Serialization.JsonConverter`1[[LaunchMyApp.Client.IMe_Me, LaunchMyApp.Client, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
//    at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1[[LaunchMyApp.Client.IMe_Me, LaunchMyApp.Client, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].Deserialize(Utf8JsonReader& reader, ReadStack& state)
//    at System.Text.Json.JsonSerializer.Read[IMe_Me](Utf8JsonReader& reader, JsonTypeInfo`1 jsonTypeInfo)
//    at System.Text.Json.JsonSerializer.Deserialize[IMe_Me](Utf8JsonReader& reader, JsonSerializerOptions options)
//    at Microsoft.AspNetCore.Components.PersistentComponentState.TryTakeFromJson[IMe_Me](String key, IMe_Me& instance)
//    at LaunchMyApp.Client.UserProfileDetail.View.OnInitializedAsync() in C:\src\launch-my-app\LaunchMyApp.Client\UserProfileDetail\View.razor:line 22
//    at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
        if (ApplicationState.TryTakeFromJson<IMe_Me>(nameof(Result), out var restoredData))
        {
            Result = restoredData;
        }
        else
        {
            Result = (await Client.Me.ExecuteAsync()).Data?.Me;
        }
        
        _subscription = Client.Me.Watch().Subscribe(result =>
        {
            Result = result.Data?.Me;
            InvokeAsync(StateHasChanged);
        });
        
        await base.OnInitializedAsync();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender)
        {
            _isOnClient = true;
        }
        
        Result = (await Client.Me.ExecuteAsync()).Data?.Me;
        
        await base.OnAfterRenderAsync(firstRender);
    }

    private Task PersistData()
    {
        ApplicationState.PersistAsJson(nameof(Result), Result);

        return Task.CompletedTask;
    }

    public IMe_Me? Result { get; set; }

    public void Dispose()
    {
        persistingSubscription.Dispose();
        _subscription?.Dispose();
    }
}

The solution you'd like

  1. I'd like to be able to convert the return value from StrawberryShake's ExecuteAsync() method to and from JSON.
  2. Nice to have: I'd like some way to have the two transport profiles be generated in two separate C# projects.

nloum avatar Jun 29 '24 18:06 nloum