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

[StrawberryShake] AddScopedXClient

Open repne opened this issue 1 year ago • 4 comments

Summary

Adds a new method to the service collection extension to register the client as scoped as opposed to singleton.

Why? This is so a client generated for a Blazor WebAssembly project can be shared with the server hosting it. Sharing it as opposed to use a newly generated client with NoStore. This way a developer can reuse components and services between client side rendering and server side prerending. Initially I tried to get a PR for having a client generated with two modes, one for the WebAssembly client (noStore = false) and one for the server side Aspnet (noStore = true). However this proved too difficult because data types were generated differently and there was a lot of mapping involved (entityIds vs entities) so sharing the components was not easy. With this new approach there's the side effect that entityIds are also generated on the server (unlike the noStore = true approach) and so cache hydration from a prerendered page is easier to accomplish.

A followup PR will implement prerendering, stream rendering, and cache hydration of the Blazor components, I already have a working PoC.

This is what the new method look like with all the namespaces removed:

public static IScopedClientBuilder<FooClientStoreAccessor> AddScopedFooClient(this IServiceCollection services, ExecutionStrategy strategy = ExecutionStrategy.NetworkOnly, global::System.Action<IServiceCollection>? configureClientServices = null)
{
    ServiceCollectionServiceExtensions.AddScoped(services, sp =>
    {
        var serviceCollection = new ServiceCollection();
        ConfigureClientDefault(sp, serviceCollection, strategy);
        if (configureClientServices is not null)
        {
            configureClientServices(serviceCollection);
        }

        return new ClientServiceProvider(ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(serviceCollection));
    });
    ServiceCollectionServiceExtensions.AddScoped(services, sp => new FooClientStoreAccessor(ServiceProviderServiceExtensions.GetRequiredService<IOperationStore>(ServiceProviderServiceExtensions.GetRequiredService<ClientServiceProvider>(sp)), ServiceProviderServiceExtensions.GetRequiredService<IEntityStore>(ServiceProviderServiceExtensions.GetRequiredService<ClientServiceProvider>(sp)), ServiceProviderServiceExtensions.GetRequiredService<IEntityIdSerializer>(ServiceProviderServiceExtensions.GetRequiredService<ClientServiceProvider>(sp)), ServiceProviderServiceExtensions.GetRequiredService<global::System.Collections.Generic.IEnumerable<IOperationRequestFactory>>(ServiceProviderServiceExtensions.GetRequiredService<ClientServiceProvider>(sp)), ServiceProviderServiceExtensions.GetRequiredService<global::System.Collections.Generic.IEnumerable<IOperationResultDataFactory>>(ServiceProviderServiceExtensions.GetRequiredService<ClientServiceProvider>(sp))));
    ServiceCollectionServiceExtensions.AddScoped(services, sp => ServiceProviderServiceExtensions.GetRequiredService<GetPersonQuery>(ServiceProviderServiceExtensions.GetRequiredService<ClientServiceProvider>(sp)));
    ServiceCollectionServiceExtensions.AddScoped(services, sp => ServiceProviderServiceExtensions.GetRequiredService<OnPersonSubscription>(ServiceProviderServiceExtensions.GetRequiredService<ClientServiceProvider>(sp)));
    ServiceCollectionServiceExtensions.AddScoped(services, sp => ServiceProviderServiceExtensions.GetRequiredService<CreatePersonMutation>(ServiceProviderServiceExtensions.GetRequiredService<ClientServiceProvider>(sp)));
    ServiceCollectionServiceExtensions.AddScoped(services, sp => ServiceProviderServiceExtensions.GetRequiredService<FooClient>(ServiceProviderServiceExtensions.GetRequiredService<ClientServiceProvider>(sp)));
    ServiceCollectionServiceExtensions.AddScoped(services, sp => ServiceProviderServiceExtensions.GetRequiredService<IFooClient>(ServiceProviderServiceExtensions.GetRequiredService<ClientServiceProvider>(sp)));
    return new ScopedClientBuilder<FooClientStoreAccessor>("FooClient", services);
}

repne avatar Dec 29 '24 18:12 repne

This is a proof of concept on how to implement cache hydration, stream rendering, and prerendering: https://gist.github.com/repne/2646d29fff4c959a91bc7de8986b2882

In order to do this, the same strawberry client needs to be used on both server and client Blazor projects.

This can be tested using the following project:

dotnet new blazor -e -int Auto -ai -o MyProject.Web

repne avatar Dec 30 '24 17:12 repne

I have a draft PR that builds on top of this and adds the hydrate functionality and pre-rendering support: https://github.com/repne/hotchocolate/pull/1 Let me know if I'm on the right track and I will get it ready.

repne avatar Dec 31 '24 12:12 repne

Hey, we will soon redo StrawberryShake to cater to the newer capabilities in Blazor ... If you want you can join the effort.

michaelstaib avatar May 22 '25 18:05 michaelstaib

@michaelstaib does it make sense that I pick this up again? This is step one to enable server side pre-rendering in Blazor.

repne avatar Oct 08 '25 19:10 repne