ChilliCream / ChilliCream/graphql-platform

Make StrawberryShake play nicely with Blazor's PersistentComponentState

Open
#7,209 1 comment 4 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

🌶️ strawberry shake
Dominant language
C#
Stars
5.8k
Forks
810
Avg merge
15h 39m
Merged PRs (30d)
98

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start from the StrawberryShake ExecuteAsync result used as IMe_Me in the View.razor example, and compare it with Blazor PersistentComponentState's TryTakeFromJson and PersistAsJson entry points. Define what serialization support and separate transport-profile generation should expose, then verify the result can round-trip the query data and support the requested server/client setup.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, graphql
Domain
frontend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.