From 59eccd99b907fcaf66dae52b52c8098d07434e01 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 5 Sep 2022 10:53:37 +0200 Subject: [PATCH] Added version docs for StrawberryShake --- website/src/docs/docs.json | 92 ++- .../docs/strawberryshake/get-started/index.md | 4 + .../strawberryshake/v12/caching/entities.md | 1 + .../docs/strawberryshake/v12/caching/index.md | 67 ++ .../v12/caching/invalidation.md | 1 + .../docs/strawberryshake/v12/configuration.md | 30 + .../v12/get-started/console.md | 211 ++++++ .../strawberryshake/v12/get-started/index.md | 707 ++++++++++++++++++ .../v12/get-started/xamarin.md | 406 ++++++++++ website/src/docs/strawberryshake/v12/index.md | 15 + .../v12/networking/authentication.md | 156 ++++ .../strawberryshake/v12/networking/index.md | 63 ++ .../strawberryshake/v12/performance/index.md | 36 + .../v12/performance/persisted-queries.md | 39 + .../v12/performance/persisted-state.md | 7 + .../src/docs/strawberryshake/v12/scalars.md | 204 +++++ .../docs/strawberryshake/v12/subscriptions.md | 55 ++ .../src/docs/strawberryshake/v12/tooling.md | 66 ++ 18 files changed, 2159 insertions(+), 1 deletion(-) create mode 100644 website/src/docs/strawberryshake/v12/caching/entities.md create mode 100644 website/src/docs/strawberryshake/v12/caching/index.md create mode 100644 website/src/docs/strawberryshake/v12/caching/invalidation.md create mode 100644 website/src/docs/strawberryshake/v12/configuration.md create mode 100644 website/src/docs/strawberryshake/v12/get-started/console.md create mode 100644 website/src/docs/strawberryshake/v12/get-started/index.md create mode 100644 website/src/docs/strawberryshake/v12/get-started/xamarin.md create mode 100644 website/src/docs/strawberryshake/v12/index.md create mode 100644 website/src/docs/strawberryshake/v12/networking/authentication.md create mode 100644 website/src/docs/strawberryshake/v12/networking/index.md create mode 100644 website/src/docs/strawberryshake/v12/performance/index.md create mode 100644 website/src/docs/strawberryshake/v12/performance/persisted-queries.md create mode 100644 website/src/docs/strawberryshake/v12/performance/persisted-state.md create mode 100644 website/src/docs/strawberryshake/v12/scalars.md create mode 100644 website/src/docs/strawberryshake/v12/subscriptions.md create mode 100644 website/src/docs/strawberryshake/v12/tooling.md diff --git a/website/src/docs/docs.json b/website/src/docs/docs.json index 767f98f9fb2..8eb64644bfa 100644 --- a/website/src/docs/docs.json +++ b/website/src/docs/docs.json @@ -860,7 +860,97 @@ "versions": [ { "path": "", - "title": "v11", + "title": "v13", + "items": [ + { + "path": "index", + "title": "Introduction" + }, + { + "path": "get-started", + "title": "Get Started", + "items": [ + { + "path": "index", + "title": "Blazor" + }, + { + "path": "xamarin", + "title": "Xamarin" + }, + { + "path": "console", + "title": "Console" + } + ] + }, + { + "path": "subscriptions", + "title": "Subscriptions" + }, + { + "path": "tooling", + "title": "Tooling / CLI" + }, + { + "path": "caching", + "title": "Caching", + "items": [ + { + "path": "index", + "title": "Overview" + }, + { + "path": "entities", + "title": "Entities" + }, + { + "path": "invalidation", + "title": "Invalidation" + } + ] + }, + { + "path": "performance", + "title": "Performance", + "items": [ + { + "path": "index", + "title": "Overview" + }, + { + "path": "persisted-queries", + "title": "Persisted Queries" + }, + { + "path": "persisted-state", + "title": "Persisted State" + } + ] + }, + { + "path": "networking", + "title": "Networking", + "items": [ + { + "path": "index", + "title": "Protocols" + }, + { + "path": "authentication", + "title": "Authentication" + } + ] + }, + { + "path": "scalars", + "title": "Scalars" + } + ] + }, + { + "path": "v12", + "title": "v12", "items": [ { "path": "index", diff --git a/website/src/docs/strawberryshake/get-started/index.md b/website/src/docs/strawberryshake/get-started/index.md index 4016e43e3b6..3f603abe91b 100644 --- a/website/src/docs/strawberryshake/get-started/index.md +++ b/website/src/docs/strawberryshake/get-started/index.md @@ -14,6 +14,10 @@ In this tutorial, we will teach you: - How to generate source code from .graphql files, that contain operations. - How to use the generated client in a classical or reactive way. + + # Step 1: Add the Strawberry Shake CLI tools The Strawberry Shake tool will help you to setup your project to create a GraphQL client. diff --git a/website/src/docs/strawberryshake/v12/caching/entities.md b/website/src/docs/strawberryshake/v12/caching/entities.md new file mode 100644 index 00000000000..3d606d657ef --- /dev/null +++ b/website/src/docs/strawberryshake/v12/caching/entities.md @@ -0,0 +1 @@ +> We are still working on the documentation for Strawberry Shake so help us by finding typos, missing things or write some additional docs with us. diff --git a/website/src/docs/strawberryshake/v12/caching/index.md b/website/src/docs/strawberryshake/v12/caching/index.md new file mode 100644 index 00000000000..eec3f9173b6 --- /dev/null +++ b/website/src/docs/strawberryshake/v12/caching/index.md @@ -0,0 +1,67 @@ +--- +title: "Caching" +--- + +> We are still working on the documentation for Strawberry Shake, so help us by finding typos, missing things, or write some additional docs with us. + +StrawberryShake stores the result of GraphQL requests in a normalized entity store. The entity store allows your client to execute GraphQL requests with various strategies to reduce the need for network requests. Moreover, the normalized entities are updated by every request the client does, which means that you can build fully reactive components that change as the state in the store changes. + +```mermaid +sequenceDiagram + participant Generated Client + participant Operation Store + participant Entity Store + participant GraphQL Server + Generated Client->>Operation Store: Queries local store + Operation Store->>GraphQL Server: Queries GraphQL server + Note over Entity Store: Normalize response into entities + GraphQL Server->>Entity Store: Returns GraphQL response + Note over Operation Store: Builds operation result from entities + Entity Store->>Operation Store: Returns entities for operation + Operation Store->>Generated Client: Returns operation result +``` + +# Strategies + +We support three basic strategies to interact with the store and fetch data. + +## Network Only + +Network only is the simplest strategy and will fetch from the network and only then update the store. This means that our initial call will always get fresh data and at the same time update other request results watching the same entities. + +If we use the reactive APIs in combination with network only we will still get updates whenever other requests fetch data for the entities we are watching. + +## Cache First + +Cache first is essentially the opposite of network only since it will first fetch from the store, and if the store has the data needed, it will not make any network requests. If the store does not have the data needed, it will go to the network and try to get the data and update the store. + +## Cache and Network + +The last strategy is a combination of the first two. The client will first try to get the data from the store. This gives us fast data response time if the store already has the data. After that the store will update that data for this request with data from the network which in consequence will trigger our subscription and serve us new data. + +## Configuration + +The global strategy default can be set on our dependency injection setup method. + +```csharp +builder.Services + .AddConferenceClient(ExecutionStrategy.CacheFirst) // <---- + .ConfigureHttpClient(client => client.BaseAddress = new Uri("http://localhost:5050/graphql")) + .ConfigureWebSocketClient(client => client.Uri = new Uri("ws://localhost:5050/graphql")); +``` + +The global strategy default can then be overwritten by any `Watch` method for a particular request. + +```csharp +storeSession = + ConferenceClient + .GetSessions + .Watch(ExecutionStrategy.CacheFirst) // <---- + .Where(t => !t.Errors.Any()) + .Select(t => t.Data!.Sessions!.Nodes) + .Subscribe(result => + { + sessions = result; + StateHasChanged(); + }); +``` diff --git a/website/src/docs/strawberryshake/v12/caching/invalidation.md b/website/src/docs/strawberryshake/v12/caching/invalidation.md new file mode 100644 index 00000000000..3d606d657ef --- /dev/null +++ b/website/src/docs/strawberryshake/v12/caching/invalidation.md @@ -0,0 +1 @@ +> We are still working on the documentation for Strawberry Shake so help us by finding typos, missing things or write some additional docs with us. diff --git a/website/src/docs/strawberryshake/v12/configuration.md b/website/src/docs/strawberryshake/v12/configuration.md new file mode 100644 index 00000000000..e4d67c3a48a --- /dev/null +++ b/website/src/docs/strawberryshake/v12/configuration.md @@ -0,0 +1,30 @@ +# Configuring Strawberry Shake + +Strawberry Shake is configured by altering the `.graphqlrc.json` at the root of your project. +All settings to into `extensions.strawberryShake` object. +Here is a full configuration with all possibilities: + +```json +{ + // The path of the schema file, that will be used to generate the client. + // This setting may also be used by other tooling, because it is a default field of graphql-spec + "schema": "schema.graphql", + // The selector that determines, what files will be regarded as graphql documents + // This setting may also be used by other tooling, because it is a default field of graphql-spec + "documents": "**/*.graphql", + "extensions": { + // Here do only Strawberry Shake specific settings live + "strawberryShake": { + // The name of the generated client + "name": "ChatClient", + // The namespace of all the generated files of the client + "namespace": "Demo", + // The URL of the GraphQL api you want to consume with the client + "url": "https://workshop.chillicream.com/graphql/", + // Shall your client be based on dependency injection? If yes, all needed setup code + // will be generated for you, so that you only have to add the client to your DI container. + "dependencyInjection": true + } + } +} +``` diff --git a/website/src/docs/strawberryshake/v12/get-started/console.md b/website/src/docs/strawberryshake/v12/get-started/console.md new file mode 100644 index 00000000000..9a88f1de3ad --- /dev/null +++ b/website/src/docs/strawberryshake/v12/get-started/console.md @@ -0,0 +1,211 @@ +--- +title: "Get started with Strawberry Shake in a Console application" +--- + +> We are still working on the documentation for Strawberry Shake so help us by finding typos, missing things or write some additional docs with us. + +In this tutorial we will walk you through the basics of adding a Strawberry Shake GraphQL client to a console project. For this example we will create a simple console application and fetch some simple data from our demo backend. + +Strawberry Shake is not limited to console application and can be used with any .NET standard compliant library. + +In this tutorial, we will teach you: + +- How to add the Strawberry Shake CLI tools. +- How to generate source code from .graphql files, that contain operations. +- How to use the generated client in a classical or reactive way. +- How to disable state management for ASP.NET core use-cases. + +## Step 1: Add the Strawberry Shake CLI tools + +The Strawberry Shake tool will help you to setup your project to create a GraphQL client. + +Open your preferred terminal and select a directory where you want to add the code of this tutorial. + +1. Create a dotnet tool-manifest. + +```bash +dotnet new tool-manifest +``` + +2. Install the Strawberry Shake tools. + +```bash +dotnet tool install StrawberryShake.Tools --local +``` + +## Step 2: Create a console project + +Next, we will create our console project so that we have a little playground. + +1. First, a new solution called `Demo.sln`. + +```bash +dotnet new sln -n Demo +``` + +2. Create a new console application. + +```bash +dotnet new console -n Demo +``` + +3. Add the project to the solution `Demo.sln`. + +```bash +dotnet sln add ./Demo +``` + +## Step 3: Install the required packages + +Strawberry Shake supports multiple GraphQL transport protocols. In this example we will use the standard GraphQL over HTTP protocol to interact with our GraphQL server. + +1. Add the `StrawberryShake.Transport.Http` package to your project. + +```bash +dotnet add Demo package StrawberryShake.Transport.Http +``` + +2. Add the `StrawberryShake.CodeGeneration.CSharp.Analyzers` package to your project in order to add our code generation. + +```bash +dotnet add Demo package StrawberryShake.CodeGeneration.CSharp.Analyzers +``` + +When using the HTTP protocol we also need the HttpClientFactory and the Microsoft dependency injection. + +3. Add the `Microsoft.Extensions.DependencyInjection` package to your project in order to add our code generation. + +```bash +dotnet add Demo package Microsoft.Extensions.DependencyInjection +``` + +3. Add the `Microsoft.Extensions.Http` package to your project in order to add our code generation. + +```bash +dotnet add Demo package Microsoft.Extensions.Http +``` + +## Step 4: Add a GraphQL client to your project using the CLI tools + +To add a client to your project, you need to run the `dotnet graphql init {{ServerUrl}} -n {{ClientName}}`. + +In this tutorial we will use our GraphQL workshop to create a list of sessions that we will add to our console application. + +> If you want to have a look at our GraphQL workshop head over [here](https://github.com/ChilliCream/graphql-workshop). + +1. Add the conference client to your console application. + +```bash +dotnet graphql init https://workshop.chillicream.com/graphql/ -n ConferenceClient -p ./Demo +``` + +2. Customize the namespace of the generated client to be `Demo.GraphQL`. For this head over to the `.graphqlrc.json` and insert a namespace property to the `StrawberryShake` section. + +```json +{ + "schema": "schema.graphql", + "documents": "**/*.graphql", + "extensions": { + "strawberryShake": { + "name": "ConferenceClient", + "namespace": "Demo.GraphQL", + "url": "https://workshop.chillicream.com/graphql/", + "dependencyInjection": true + } + } +} +``` + +Now that everything is in place let us write our first query to ask for a list of session titles of the conference API. + +3. Choose your favorite IDE and the solution. If your are using VSCode do the following: + +```bash +code ./Demo +``` + +4. Create new query document `GetSessions.graphql` with the following content: + +```graphql +query GetSessions { + sessions(order: { title: ASC }) { + nodes { + title + } + } +} +``` + +5. Compile your project. + +```bash +dotnet build +``` + +With the project compiled you now should see a directory `Generated`. The generated code is just there for the IDE, the actual code was injected directly into roslyn through source generators. + +![Visual Studio code showing the generated directory.](../../shared/berry_console_generated.png) + +1. Head over to the `Program.cs` and add the new `ConferenceClient` to the dependency injection. + +> In some IDEs it is still necessary to reload the project after the code was generated to update the IntelliSense. So, if you have any issues in the next step with IntelliSense just reload the project and everything should be fine. + +```csharp +using System; +using Microsoft.Extensions.DependencyInjection; +using Demo.GraphQL; + +namespace Demo +{ + class Program + { + static void Main(string[] args) + { + var serviceCollection = new ServiceCollection(); + + serviceCollection + .AddConferenceClient() + .ConfigureHttpClient(client => client.BaseAddress = new Uri("https://workshop.chillicream.com/graphql")); + + IServiceProvider services = serviceCollection.BuildServiceProvider(); + + IConferenceClient client = services.GetRequiredService(); + } + } +} +``` + +## Step 5: Use the ConferenceClient to perform a simple fetch + +In this section we will perform a simple fetch with our `ConferenceClient` and output the result to the console. + +1. Head over to `Program.cs`. + +2. Add the following code to your main method to execute the `GetSessions` query. + +```csharp +static async Task Main(string[] args) +{ + var serviceCollection = new ServiceCollection(); + + serviceCollection + .AddConferenceClient() + .ConfigureHttpClient(client => client.BaseAddress = new Uri("https://workshop.chillicream.com/graphql")); + + IServiceProvider services = serviceCollection.BuildServiceProvider(); + + IConferenceClient client = services.GetRequiredService(); + + var result = await client.GetSessions.ExecuteAsync(); + result.EnsureNoErrors(); + + foreach (var session in result.Data.Sessions.Nodes) + { + Console.WriteLine(session.Title); + } +} +``` + +3. Start the console application with `dotnet run --project ./Demo` and see if your code works. + +![Started console application that shows a list of sessions](../../shared/berry_console_session_list.png) diff --git a/website/src/docs/strawberryshake/v12/get-started/index.md b/website/src/docs/strawberryshake/v12/get-started/index.md new file mode 100644 index 00000000000..4016e43e3b6 --- /dev/null +++ b/website/src/docs/strawberryshake/v12/get-started/index.md @@ -0,0 +1,707 @@ +--- +title: "Get started with Strawberry Shake and Blazor" +--- + +> We are still working on the documentation for Strawberry Shake so help us by finding typos, missing things or write some additional docs with us. + +In this tutorial we will walk you through the basics of adding a Strawberry Shake GraphQL client to a Blazor for WebAssembly project. For this example we will create a Blazor for WebAssembly application and fetch some simple data from our demo backend. + +Strawberry Shake is not limited to Blazor and can be used with any .NET standard compliant library. + +In this tutorial, we will teach you: + +- How to add the Strawberry Shake CLI tools. +- How to generate source code from .graphql files, that contain operations. +- How to use the generated client in a classical or reactive way. + +# Step 1: Add the Strawberry Shake CLI tools + +The Strawberry Shake tool will help you to setup your project to create a GraphQL client. + +Open your preferred terminal and select a directory where you want to add the code of this tutorial. + +1. Create a dotnet tool-manifest. + +```bash +dotnet new tool-manifest +``` + +2. Install the Strawberry Shake tools. + +```bash +dotnet tool install StrawberryShake.Tools --local +``` + +# Step 2: Create a Blazor WebAssembly project + +Next, we will create our Blazor project so that we have a little playground. + +1. First, a new solution called `Demo.sln`. + +```bash +dotnet new sln -n Demo +``` + +2. Create a new Blazor for WebAssembly application. + +```bash +dotnet new blazorwasm -n Demo +``` + +3. Add the project to the solution `Demo.sln`. + +```bash +dotnet sln add ./Demo +``` + +# Step 3: Install the required packages + +Strawberry Shake supports multiple GraphQL transport protocols. In this example we will use the standard GraphQL over HTTP protocol to interact with our GraphQL server. + +1. Add the `StrawberryShake.Transport.Http` package to your project. + +```bash +dotnet add Demo package StrawberryShake.Transport.Http +``` + +2. Add the `StrawberryShake.CodeGeneration.CSharp.Analyzers` package to your project in order to add our code generation. + +```bash +dotnet add Demo package StrawberryShake.CodeGeneration.CSharp.Analyzers +``` + +When using the HTTP protocol we also need the HttpClientFactory and the Microsoft dependency injection. + +3. Add the `Microsoft.Extensions.DependencyInjection` package to your project in order to add our code generation. + +```bash +dotnet add Demo package Microsoft.Extensions.DependencyInjection +``` + +3. Add the `Microsoft.Extensions.Http` package to your project in order to add our code generation. + +```bash +dotnet add Demo package Microsoft.Extensions.Http +``` + +# Step 4: Add a GraphQL client to your project using the CLI tools + +To add a client to your project, you need to run the `dotnet graphql init {{ServerUrl}} -n {{ClientName}}`. + +In this tutorial we will use our GraphQL workshop to create a list of sessions that we will add to our Blazor application. + +> If you want to have a look at our GraphQL workshop head over [here](https://github.com/ChilliCream/graphql-workshop). + +1. Add the conference client to your Blazor application. + +```bash +dotnet graphql init https://workshop.chillicream.com/graphql/ -n ConferenceClient -p ./Demo +``` + +2. Customize the namespace of the generated client to be `Demo.GraphQL`. For this head over to the `.graphqlrc.json` and insert a namespace property to the `StrawberryShake` section. + +```json +{ + "schema": "schema.graphql", + "documents": "**/*.graphql", + "extensions": { + "strawberryShake": { + "name": "ConferenceClient", + "namespace": "Demo.GraphQL", + "url": "https://workshop.chillicream.com/graphql/", + "dependencyInjection": true + } + } +} +``` + +Now that everything is in place let us write our first query to ask for a list of session titles of the conference API. + +3. Choose your favorite IDE and the solution. If your are using VSCode do the following: + +```bash +code ./Demo +``` + +4. Create new query document `GetSessions.graphql` with the following content: + +```graphql +query GetSessions { + sessions(order: { title: ASC }) { + nodes { + title + } + } +} +``` + +5. Compile your project. + +```bash +dotnet build +``` + +With the project compiled you now should see a directory `Generated`. The generated code is just there for the IDE, the actual code was injected directly into roslyn through source generators. + +![Visual Studio code showing the generated directory.](../../shared/berry_generated.png) + +6. Head over to the `Program.cs` and add the new `ConferenceClient` to the dependency injection. + +> In some IDEs it is still necessary to reload the project after the code was generated to update the IntelliSense. So, if you have any issues in the next step with IntelliSense just reload the project and everything should be fine. + +```csharp +public class Program +{ + public static async Task Main(string[] args) + { + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.RootComponents.Add("#app"); + + builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + + builder.Services + .AddConferenceClient() + .ConfigureHttpClient(client => client.BaseAddress = new Uri("https://workshop.chillicream.com/graphql")); + + await builder.Build().RunAsync(); + } +} +``` + +7. Go to `_Imports.razor` and add `Demo.GraphQL` to the common imports + +```csharp +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using Demo +@using Demo.Shared +@using Demo.GraphQL +@using StrawberryShake +``` + +# Step 5: Use the ConferenceClient to perform a simple fetch + +In this section we will perform a simple fetch with our `ConferenceClient`. We will not yet look at state or other things that come with our client but just perform a simple fetch. + +1. Head over to `Pages/Index.razor`. + +2. Add inject the `ConferenceClient` beneath the `@pages` directive. + +```csharp +@page "/" +@inject ConferenceClient ConferenceClient; +``` + +3. Introduce a code directive at the bottom of the file. + +```csharp +@page "/" +@inject ConferenceClient ConferenceClient; + +

Hello, world!

+ +Welcome to your new app. + + + +@code { + +} +``` + +4. Now lets fetch the titles with our client. + +```csharp +@page "/" +@inject ConferenceClient ConferenceClient; + +

Hello, world!

+ +Welcome to your new app. + + + +@code { + private string[] titles = Array.Empty(); + + protected override async Task OnInitializedAsync() + { + var result = await ConferenceClient.GetSessions.ExecuteAsync(); + titles = result.Data.Sessions.Nodes.Select(t => t.Title).ToArray(); + } +} +``` + +5. Last, lets render the titles on our page as a list. + +```csharp +@page "/" +@inject ConferenceClient ConferenceClient; + +

Hello, world!

+ +Welcome to your new app. + + + +
    + @foreach (string title in titles) { +
  • @title
  • + } +
+ +@code { + private string[] titles = Array.Empty(); + + protected override async Task OnInitializedAsync() + { + var result = await ConferenceClient.GetSessions.ExecuteAsync(); + titles = result.Data.Sessions.Nodes.Select(t => t.Title).ToArray(); + } +} +``` + +5. Start the Blazor application with `dotnet run --project ./Demo` and see if your code works. + +![Started Blazor application in Microsoft Edge](../../shared/berry_session_list.png) + +# Step 6: Using the built-in store with reactive APIs. + +The simple fetch of our data works. But every time we visit the index page it will fetch the data again although the data does not change often. Strawberry Shake also comes with state management where you can control the entity store and update it when you need to. In order to best interact with the store we will use `System.Reactive` from Microsoft. Lets get started :) + +1. Install the package `System.Reactive`. + +```bash +dotnet add Demo package System.Reactive +``` + +2. Next, let us update the `_Imports.razor` with some more imports, namely `System`, `System.Reactive.Linq`, `System.Linq` and `StrawberryShake`. + +```csharp +@using System +@using System.Reactive.Linq +@using System.Linq +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using Demo +@using Demo.Shared +@using Demo.GraphQL +@using StrawberryShake +``` + +3. Head back to `Pages/Index.razor` and replace the code section with the following code: + +```csharp +private string[] titles = Array.Empty(); +private IDisposable storeSession; + +protected override void OnInitialized() +{ + storeSession = + ConferenceClient + .GetSessions + .Watch(StrawberryShake.ExecutionStrategy.CacheFirst) + .Where(t => !t.Errors.Any()) + .Select(t => t.Data.Sessions.Nodes.Select(t => t.Title).ToArray()) + .Subscribe(result => + { + titles = result; + StateHasChanged(); + }); +} +``` + +Instead of fetching the data we watch the data for our request. Every time entities of our results are updated in the entity store our subscribe method will be triggered. + +Also we specified on our watch method that we want to first look at the store and only if there is nothing in the store we want to fetch the data from the network. + +Last, note that we are storing a disposable on our component state called `storeSession`. This represents our session with the store. We need to dispose the session when we no longer display our component. + +4. Implement `IDisposable` and handle the `storeSession` dispose. + +```csharp +@page "/" +@inject ConferenceClient ConferenceClient; +@implements IDisposable + +

Hello, world!

+ +Welcome to your new app. + + + +
    +@foreach (var title in titles) +{ +
  • @title
  • +} +
+ +@code { + private string[] titles = Array.Empty(); + private IDisposable storeSession; + + protected override void OnInitialized() + { + storeSession = + ConferenceClient + .GetSessions + .Watch(StrawberryShake.ExecutionStrategy.CacheFirst) + .Where(t => !t.Errors.Any()) + .Select(t => t.Data.Sessions.Nodes.Select(t => t.Title).ToArray()) + .Subscribe(result => + { + titles = result; + StateHasChanged(); + }); + } + + public void Dispose() + { + storeSession?.Dispose(); + } +} +``` + +Every time we move away from our index page Blazor will dispose our page which consequently will dispose our store session. + +5. Start the Blazor application with `dotnet run --project ./Demo` and see if your code works. + +![Started Blazor application in Microsoft Edge](../../shared/berry_session_list.png) + +The page will look unchanged. + +6. Next, open the developer tools of your browser and switch to the developer tools console. Refresh the site so that we get a fresh output. + +![Microsoft Edge developer tools show just one network interaction.](../../shared/berry_session_list_network.png) + +7. Switch between the `Index` and the `Counter` page (back and forth) and watch the console output. + +The Blazor application just fetched a single time from the network and now only gets the data from the store. + +# Step 7: Using GraphQL mutations + +In this step we will introduce a mutation that will allow us to rename a session. For this we need to change our Blazor page a bit. + +1. We need to get the session id for our session so that we can call the `renameSession` mutation. For this we will rewrite our `GetSessions` operation. + +```graphql +query GetSessions { + sessions(order: { title: ASC }) { + nodes { + ...SessionInfo + } + } +} + +fragment SessionInfo on Session { + id + title +} +``` + +2. Next we need to restructure the `Index.razor` page. We will get rid of the Blazor default content and rework our list to use our fragment `SessionInfo`. Further, we will introduce a button to our list so that we have a hook to start editing items from our list. + +```csharp +@page "/" +@inject ConferenceClient ConferenceClient; +@implements IDisposable + +

Sessions

+ +
    +@foreach (ISessionInfo session in sessions) +{ +
  • @session.Title
  • +} +
+ +@code { + private IReadOnlyList sessions = Array.Empty(); + private IDisposable storeSession; + + protected override void OnInitialized() + { + storeSession = + ConferenceClient + .GetSessions + .Watch(ExecutionStrategy.CacheFirst) + .Where(t => !t.Errors.Any()) + .Select(t => t.Data!.Sessions!.Nodes) + .Subscribe(result => + { + sessions = result; + StateHasChanged(); + }); + } + + private void OnClickSession(ISessionInfo session) + { + + } + + public void Dispose() + { + storeSession?.Dispose(); + } +} +``` + +3. Next, we will define the GraphQL mutation by adding a new GraphQL document `RenameSession.graphql`. + +```graphql +mutation RenameSession($sessionId: ID!, $title: String!) { + renameSession(input: { sessionId: $sessionId, title: $title }) { + session { + ...SessionInfo + } + } +} +``` + +4. Rebuild, the project so that the source generator will create all our new types. + +5. Go back to the `Index.razor` page and lets add some state for our edit controls. + +```csharp +private ISessionInfo selectedSession; +private string title; +``` + +The page should now look like the following: + +```csharp +@page "/" +@inject ConferenceClient ConferenceClient; +@implements IDisposable + +

Sessions

+ +
    +@foreach (ISessionInfo session in sessions) +{ +
  • @session.Title
  • +} +
+ +@code { + private IReadOnlyList sessions = Array.Empty(); + private IDisposable storeSession; + private ISessionInfo selectedSession; + private string title; + + protected override void OnInitialized() + { + storeSession = + ConferenceClient + .GetSessions + .Watch(ExecutionStrategy.CacheFirst) + .Where(t => !t.Errors.Any()) + .Select(t => t.Data!.Sessions!.Nodes) + .Subscribe(result => + { + sessions = result; + StateHasChanged(); + }); + } + + private void OnClickSession(ISessionInfo session) + { + + } + + public void Dispose() + { + storeSession?.Dispose(); + } +} +``` + +6. Now, lets put some controls in to let the user edit the title of one of our sessions. + +```csharp +@if (selectedSession is not null) +{ +
+

Edit Session Title:

+ + +} +``` + +The page should now look like the following: + +```csharp +@page "/" +@inject ConferenceClient ConferenceClient; +@implements IDisposable + +

Sessions

+ +
    +@foreach (ISessionInfo session in sessions) +{ +
  • @session.Title
  • +} +
+ +@if (selectedSession is not null) +{ +
+

Edit Session Title:

+ + +} + +@code { + private IReadOnlyList sessions = Array.Empty(); + private IDisposable storeSession; + private ISessionInfo selectedSession; + private string title; + + protected override void OnInitialized() + { + storeSession = + ConferenceClient + .GetSessions + .Watch(ExecutionStrategy.CacheFirst) + .Where(t => !t.Errors.Any()) + .Select(t => t.Data!.Sessions!.Nodes) + .Subscribe(result => + { + sessions = result; + StateHasChanged(); + }); + } + + private void OnClickSession(ISessionInfo session) + { + + } + + public void Dispose() + { + storeSession?.Dispose(); + } +} +``` + +7. Next, we want to wire the controls up with the click. For that replace the `OnClickSession` method with the following code: + +```csharp +private void OnClickSession(ISessionInfo session) +{ + selectedSession = session; + title = session.Title; + StateHasChanged(); +} +``` + +8. Add, a new method that now executes our new mutation `RenameSession`. + +```csharp +private async Task OnSaveTitle() +{ + await ConferenceClient.RenameSession.ExecuteAsync(selectedSession.Id, title); + selectedSession = null; + title = null; + StateHasChanged(); +} +``` + +The page should now look like the following: + +```csharp +@page "/" +@inject ConferenceClient ConferenceClient; +@implements IDisposable + +

Sessions

+ +
    +@foreach (ISessionInfo session in sessions) +{ +
  • @session.Title
  • +} +
+ +@if (selectedSession is not null) +{ +
+

Edit Session Title:

+ + +} + +@code { + private IReadOnlyList sessions = Array.Empty(); + private IDisposable storeSession; + private ISessionInfo selectedSession; + private string title; + + protected override void OnInitialized() + { + storeSession = + ConferenceClient + .GetSessions + .Watch(ExecutionStrategy.CacheFirst) + .Where(t => !t.Errors.Any()) + .Select(t => t.Data!.Sessions!.Nodes) + .Subscribe(result => + { + sessions = result; + StateHasChanged(); + }); + } + + private void OnClickSession(ISessionInfo session) + { + selectedSession = session; + title = session.Title; + StateHasChanged(); + } + + private async Task OnSaveTitle() + { + await ConferenceClient.RenameSession.ExecuteAsync(selectedSession.Id, title); + selectedSession = null; + title = null; + StateHasChanged(); + } + + public void Dispose() + { + storeSession?.Dispose(); + } +} +``` + +9. Start the Blazor application with `dotnet run --project ./Demo` and see if your code works. + +![Started Blazor application in Microsoft Edge](../../shared/berry_mutation_1.png) + +10. Click on the edit button of one of the sessions. + +![Clicked on session edit button](../../shared/berry_mutation_2.png) + +11. Change the title of the session and click save. + +![Clicked on session edit button](../../shared/berry_mutation_3.png) + +11. The item is now changed in the list although we have not explicitly written any code to update the item in our list component. + +![Clicked on session edit button](../../shared/berry_mutation_4.png) + +Strawberry Shake knows about your entities and how they are connected. Whenever one request updates the state, all components referring to data of the linked entities are updated. diff --git a/website/src/docs/strawberryshake/v12/get-started/xamarin.md b/website/src/docs/strawberryshake/v12/get-started/xamarin.md new file mode 100644 index 00000000000..db1a2fd4515 --- /dev/null +++ b/website/src/docs/strawberryshake/v12/get-started/xamarin.md @@ -0,0 +1,406 @@ +--- +title: "Get started with Strawberry Shake and Xamarin" +--- + +> We are still working on the documentation for Strawberry Shake so help us by finding typos, missing things or write some additional docs with us. + +In this tutorial we will walk you through the basics of adding a Strawberry Shake GraphQL client to a .NET project. For this example we will create a Blazor for WebAssembly application and fetch some simple data from our demo backend. + +Strawberry Shake is not limited to Blazor and can be used with any .NET standard compliant library. + +In this tutorial, we will teach you: + +- How to add the Strawberry Shake CLI tools. +- How to generate source code from .graphql files, that contain operations. +- How to use the generated client in a classical or reactive way. + +## Step 1: Add the Strawberry Shake CLI tools + +The Strawberry Shake tool will help you to setup your project to create a GraphQL client. + +Open your preferred terminal and select a directory where you want to add the code of this tutorial. + +1. Create a dotnet tool-manifest. + +```bash +dotnet new tool-manifest +``` + +2. Install the Strawberry Shake tools. + +```bash +dotnet tool install StrawberryShake.Tools --local +``` + +## Step 2: Create a Blazor WebAssembly project + +Next, we will create our Blazor project so that we have a little playground. + +1. First, a new solution called `Demo.sln`. + +```bash +dotnet new sln -n Demo +``` + +2. Create a new Blazor for WebAssembly application. + +```bash +dotnet new wasm -n Demo +``` + +3. Add the project to the solution `Demo.sln`. + +```bash +dotnet sln add ./Demo +``` + +## Step 3: Install the required packages + +Strawberry Shake supports multiple GraphQL transport protocols. In this example we will use the standard GraphQL over HTTP protocol to interact with our GraphQL server. + +1. Add the `StrawberryShake.Transport.Http` package to your project. + +```bash +dotnet add Demo package StrawberryShake.Transport.Http +``` + +2. Add the `StrawberryShake.CodeGeneration.CSharp.Analyzers` package to your project in order to add our code generation. + +```bash +dotnet add Demo package StrawberryShake.CodeGeneration.CSharp.Analyzers +``` + +When using the HTTP protocol we also need the HttpClientFactory and the Microsoft dependency injection. + +3. Add the `Microsoft.Extensions.DependencyInjection` package to your project in order to add our code generation. + +```bash +dotnet add Demo package Microsoft.Extensions.DependencyInjection +``` + +3. Add the `Microsoft.Extensions.Http` package to your project in order to add our code generation. + +```bash +dotnet add Demo package Microsoft.Extensions.Http +``` + +## Step 4: Add a GraphQL client to your project using the CLI tools + +To add a client to your project, you need to run the `dotnet graphql init {{ServerUrl}} -n {{ClientName}}`. + +In this tutorial we will use our GraphQL workshop to create a list of sessions that we will add to our Blazor application. + +> If you want to have a look at our GraphQL workshop head over [here](https://github.com/ChilliCream/graphql-workshop). + +1. Add the conference client to your Blazor application. + +```bash +dotnet graphql init https://workshop.chillicream.com/graphql/ -n ConferenceClient -p ./Demo +``` + +2. Customize the namespace of the generated client to be `Demo.GraphQL`. For this head over to the `.graphqlrc.json` and insert a namespace property to the `StrawberryShake` section. + +```json +{ + "schema": "schema.graphql", + "documents": "**/*.graphql", + "extensions": { + "strawberryShake": { + "name": "ConferenceClient", + "namespace": "Demo.GraphQL", + "url": "https://workshop.chillicream.com/graphql/", + "dependencyInjection": true + } + } +} +``` + +Now that everything is in place let us write our first query to ask for a list of session titles of the conference API. + +3. Choose your favorite IDE and the solution. If your are using VSCode do the following: + +```bash +code ./Demo +``` + +4. Create new query document `GetSessions.graphql` with the following content: + +```graphql +query GetSessions { + sessions(order: { title: ASC }) { + nodes { + title + } + } +} +``` + +5. Compile your project. + +```bash +dotnet build +``` + +With the project compiled you now should see a directory `Generated`. The generated code is just there for the IDE, the actual code was injected directly into roslyn through source generators. + +![Visual Studio code showing the generated directory.](../shared/berry_generated.png) + +6. Head over to the `Program.cs` and add the new `ConferenceClient` to the dependency injection. + +> In some IDEs it is still necessary to reload the project after the code was generated to update the IntelliSense. So, if you have any issues in the next step with IntelliSense just reload the project and everything should be fine. + +```csharp +public class Program +{ + public static async Task Main(string[] args) + { + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.RootComponents.Add("#app"); + + builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + + builder.Services + .AddConferenceClient() + .ConfigureHttpClient(client => client.BaseAddress = new Uri("https://workshop.chillicream.com/graphql")); + + await builder.Build().RunAsync(); + } +} +``` + +7. Go to `_Imports.razor` and add `Demo.GraphQL` to the common imports + +```razor +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using Demo +@using Demo.Shared +@using Demo.GraphQL +``` + +## Step 5: Use the ConferenceClient to perform a simple fetch + +In this section we will perform a simple fetch with our `ConferenceClient`. We will not yet look at state or other things that come with our client but just perform a simple fetch. + +1. Head over to `Pages/Index.razor`. + +2. Add inject the `ConferenceClient` beneath the `@pages` directive. + +```html +@page "/" @inject ConferenceClient ConferenceClient; +``` + +3. Introduce a code directive at the bottom of the file. + +```html +@page "/" @inject ConferenceClient ConferenceClient; + +

Hello, world!

+ +Welcome to your new app. + + + +@code { } +``` + +4. Now lets fetch the titles with our client. + +```html +@page "/" @inject ConferenceClient ConferenceClient; + +

Hello, world!

+ +Welcome to your new app. + + + +@code { private string[] titles = Array.Empty(); protected override async Task OnInitializedAsync() { // Execute our + GetSessions query var result = await + ConferenceClient.GetSessions.ExecuteAsync(); // aggregate the titles from the + result titles = result.Data.Sessions.Nodes.Select(t => t.Title).ToArray(); // + signal the components that the state has changed. StateHasChanged(); } + } +``` + +5. Last, lets render the titles on our page as a list. + +```html +@page "/" @inject ConferenceClient ConferenceClient; + +

Hello, world!

+ +Welcome to your new app. + + + +
    + @foreach (string title in titles) { +
  • @title
  • + } +
+ +@code { private string[] titles = Array.Empty(); protected override async Task OnInitializedAsync() { // Execute our + GetSessions query var result = await + ConferenceClient.GetSessions.ExecuteAsync(); // aggregate the titles from the + result titles = result.Data.Sessions.Nodes.Select(t => t.Title).ToArray(); // + signal the components that the state has changed. StateHasChanged(); } + } +``` + +5. Start the Blazor application with `dotnet run --project ./Demo` and see if your code works. + +![Started Blazor application in Microsoft Edge](../shared/berry_session_list.png) + +## Step 6: Using the built-in store with reactive APIs. + +The simple fetch of our data works. But every time we visit the index page it will fetch the data again although the data does not change often. Strawberry Shake also comes with state management where you can control the entity store and update it when you need to. In order to best interact with the store we will use `System.Reactive` from Microsoft. Lets get started :) + +1. Install the package `System.Reactive`. + +```bash +dotnet add Demo package System.Reactive +``` + +2. Next, let us update the `_Imports.razor` with some more imports, namely `System`, `System.Reactive.Linq`, `System.Linq` and `StrawberryShake`. + +```csharp +@using System +@using System.Reactive.Linq +@using System.Linq +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using Demo +@using Demo.Shared +@using Demo.GraphQL +@using StrawberryShake +``` + +3. Head back to `Pages/Index.razor` and replace the code section with the following code: + +```csharp +private string[] titles = Array.Empty(); +private IDisposable storeSession; + +protected override void OnInitialized() +{ + storeSession = + ConferenceClient + .GetSessions + .Watch(StrawberryShake.ExecutionStrategy.CacheFirst) + .Where(t => !t.Errors.Any()) + .Select(t => t.Data.Sessions.Nodes.Select(t => t.Title).ToArray()) + .Subscribe(result => + { + titles = result; + StateHasChanged(); + }); +} +``` + +Instead of fetching the data we watch the data for our request. Every time entities of our results are updated in the entity store our subscribe method will be triggered. + +Also we specified on our watch method that we want to first look at the store and only of there is nothing in the store we want to fetch the data from the network. + +Last, note that we are storing a disposable on our component state called `storeSession`. This represents our session with the store. We need to dispose the session when we no longer display our component. + +4. Implement `IDisposable` and handle the `storeSession` dispose. + +```csharp +@page "/" +@inject ConferenceClient ConferenceClient; +@implements IDisposable + +

Hello, world!

+ +Welcome to your new app. + + + +
    +@foreach (var title in titles) +{ +
  • @title
  • +} +
+ +@code { + private string[] titles = Array.Empty(); + private IDisposable storeSession; + + protected override void OnInitialized() + { + storeSession = + ConferenceClient + .GetSessions + .Watch(StrawberryShake.ExecutionStrategy.CacheFirst) + .Where(t => !t.Errors.Any()) + .Select(t => t.Data.Sessions.Nodes.Select(t => t.Title).ToArray()) + .Subscribe(result => + { + titles = result; + StateHasChanged(); + }); + } + + public void Dispose() + { + storeSession?.Dispose(); + } +} +``` + +Every time we move away from our index page Blazor will dispose our page which consequently will dispose our store session. + +5. Start the Blazor application with `dotnet run --project ./Demo` and see if your code works. + +![Started Blazor application in Microsoft Edge](../shared/berry_session_list.png) + +The page will look unchanged. + +6. Next, open the developer tools of your browser and switch to the developer tools console. Refresh the site so that we get a fresh output. + +![Microsoft Edge developer tools show just one network interaction.](../shared/berry_session_list_network.png) + +7. Switch between the `Index` and the `Counter` page (back and forth) and watch the console output. + +The Blazor application just fetched a single time from the network and now only gets the data from the store. + +## Step 7: Using GraphQL mutations + +In this step we will introduce a mutation that will allow us to rename a session. For this we need to change our Blazor page a bit. + +1. We need to get the session id for our session so that we can call the `renameSession` mutation. For this we will rewrite our `GetSessions` operation. + +```graphql +query GetSessions { + sessions(order: { title: ASC }) { + nodes { + ...SessionInfo + } + } +} + +fragment SessionInfo on Session { + id + title +} +``` + +2. Next we need to restructure the `Index.razor` page. diff --git a/website/src/docs/strawberryshake/v12/index.md b/website/src/docs/strawberryshake/v12/index.md new file mode 100644 index 00000000000..ab76804941a --- /dev/null +++ b/website/src/docs/strawberryshake/v12/index.md @@ -0,0 +1,15 @@ +# Introduction + +> We are still working on the documentation for Strawberry Shake so help us by finding typos, missing things or write some additional docs with us. + +Strawberry Shake is an open-source GraphQL client that is compliant with the newest GraphQL draft spec, which makes Strawberry Shake compatible with all GraphQL compliant servers like Hot Chocolate, Apollo Server, GraphQL Java and various other servers out there. + +Strawberry Shake removes the complexity of state management and lets you interact with local and remote data through GraphQL. + +You can use Strawberry Shake to: + +- Generate a C# client from your GraphQL queries. +- Interact with local and remote data through GraphQL. +- Use reactive APIs to interact with your state. + +Let's [get started](/docs/strawberryshake/get-started) with Strawberry Shake! diff --git a/website/src/docs/strawberryshake/v12/networking/authentication.md b/website/src/docs/strawberryshake/v12/networking/authentication.md new file mode 100644 index 00000000000..776b36162a4 --- /dev/null +++ b/website/src/docs/strawberryshake/v12/networking/authentication.md @@ -0,0 +1,156 @@ +--- +title: "Authentication" +--- + +To access a protected API with Strawberry Shake, you need to proof the user's identity to the server. +Each network protocol of Strawberry Shake handles authentication a bit different. + +# HTTP + +Strawberry Shake uses the `HttpClientFactory` to generate a `HttpClient` on every request. +You can either register a `HttpClient` directly on the `ServiceCollection` or use the `ConfigureHttpClient` method on the client builder. + +## ConfigureHttpClient + +The generated extension method to register the client on the serivce collection, returns a builder that can be used to configure the http client. + +```csharp + services + .AddConferenceClient() + .ConfigureHttpClient(client => + { + client.BaseAddress = + new Uri("https://workshop.chillicream.com/graphql/"); + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", "Your Oauth token"); + }); +``` + +There is an overload of the `ConfigureHttpClient` method that provides access to the `IServiceProvider`, in case the access token is stored there. + +```csharp +services + .AddConferenceClient() + .ConfigureHttpClient((serviceProvider, client) => + { + var token = serviceProvider.GetRequiredService().Token; + }); +``` + +The second parameter of `ConfigureHttpClient` allows direct access to the `HttpClientBuilder`. Use this delegate to register extensions like Polly. + +```csharp +services + .AddConferenceClient() + .ConfigureHttpClient( + client => { /*...*/ }, + builder => builder.AddPolly()); + +``` + +## HttpClientFactory + +In case you want to configure the `HttpClient` directly on the `ServiceCollection`, Strawberry Shake generates you a property `ClientName`, that you can use to set the correct name for the client. + +```csharp +services.AddHttpClient( + ConferenceClient.ClientName, + client => client.BaseAddress = + new Uri("https://workshop.chillicream.com/graphql/")); + +services.AddConferenceClient(); +``` + +# Websockets + +There are three common ways to do authentication a request over a web socket. You can either specify the authentication headers, use cookies or send the access token with the first message over the socket. +Similar to the `HttpClient`, you can configure the a web socket client over the client builder or the `ServiceCollection`. + +Strawberry Shake uses a `IWebSocketClient` that provides a similar interface as the `HttpClient` has. + +## ConfigureWebsocketClient + +You can configure the web socket client directly on the client builder after you registered it on the service collection. + +```csharp +services + .AddConferenceClient() + .ConfigureWebSocketClient(client => + { + client.Uri = new Uri("ws://localhost:" + port + "/graphql"); + client.Socket.Options.SetRequestHeader("Authorization", "Bearer ..."); + }); +``` + +You can also access the `IServiceProvider` with the following overload: + +```csharp +services + .AddConferenceClient() + .ConfigureWebSocketClient((serviceProvider, client) => + { + var token = serviceProvider.GetRequiredService().Token; + }); +``` + +The second parameter of the `ConfigureWebSocketClient` method, can be used to access the `IWebSocketClientBuilder` + +```csharp +services + .AddConferenceClient() + .ConfigureWebSocketClient( + (serviceProvider, client) => + { + var token = serviceProvider.GetRequiredService().Token; + }, + builder => + builder.ConfigureConnectionInterceptor()); +``` + +## WebSocketClientFactory + +If you prefer to use the `ServiceCollection` to configure your web socket, you can use the `AddWebSocketClient` method. Strawberry Shake generates a `ClientName` property, on each client. You can use this, to easily specify the correct name of the client. + +```csharp +services + .AddWebSocketClient( + ConferenceClient.ClientName, + client => client.Uri = + new Uri("wss://workshop.chillicream.cloud/graphql/")); + +services.AddConferenceClient(); +``` + +## IWebSocketClient + +On a `IWebSocketClient` you can configure the `Uri` of your endpoint. You can also directly set a `ISocketConnectionInterceptor` on the client, to intercept the connection and configure the initial payload. You do also have access to the underlying `ClientWebSocket` to configure headers or cookies. + +```csharp +IWebSocketClient client; +client.Uri = new Uri("wss://workshop.chillicream.cloud/graphql/"); +client.Socket.Options.SetRequestHeader("Authorization", "Bearer …"); +client.ConnectionInterceptor = new CustomConnectionInterceptor(); +``` + +## Initial payload + +In JavaScript it is not possible to add headers to a web socket. Therefor many GraphQL server do not use HTTP headers for the authentication of web sockets. Instead, they send the authentication token with the first payload to the server. + +You can specify create this payload with a `ISocketConnectionInterceptor` + +```csharp +public class CustomConnectionInterceptor + : ISocketConnectionInterceptor +{ + // the object returned by this method, will be included in the connection initialization message + public ValueTask CreateConnectionInitPayload( + ISocketProtocol protocol, + CancellationToken cancellationToken) + { + return new ValueTask( + new Dictionary { ["authToken"] = "..." }); + } +} +``` + +You can set the connection interceptor directly on the `IWebSocketClient` or on the `IWebSocketClientBuilder`. diff --git a/website/src/docs/strawberryshake/v12/networking/index.md b/website/src/docs/strawberryshake/v12/networking/index.md new file mode 100644 index 00000000000..05515338aca --- /dev/null +++ b/website/src/docs/strawberryshake/v12/networking/index.md @@ -0,0 +1,63 @@ +--- +title: "Networking" +--- + +> We are still working on the documentation for Strawberry Shake so help us by finding typos, missing things or write some additional docs with us. + +Strawberry Shake supports multiple network protocols to communicate with your GraphQL server. Each transport integration is represented by a specific NuGet package to keep your client size as small as possible. + +# Protocols + +| Transport | Protocol | Package | Strawberry Shake Version | +| --------- | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------ | +| HTTP | [GraphQL over HTTP](https://github.com/michaelstaib/graphql-over-http) | [StrawberryShake.Transport.Http](https://www.nuget.org/packages/StrawberryShake.Transport.Http) | 11.1 | +| WebSocket | [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md) | [StrawberryShake.Transport.WebSockets](https://www.nuget.org/packages/StrawberryShake.Transport.WebSockets) | 11.1 | +| WebSocket | [graphql-transport-ws](https://github.com/graphql/graphql-over-http/pull/140) | StrawberryShake.Transport.WebSockets | 12.0 | +| SignalR | GraphQL over SignalR | StrawberryShake.Transport.SignalR | 12.0 | +| gRPC | GraphQL over gRPC | StrawberryShake.Transport.gRPC | 12.0 | +| InMemory | Hot Chocolate In-Memory | [StrawberryShake.Transport.InMemory](https://www.nuget.org/packages/StrawberryShake.Transport.InMemory) | 11.1 | + +# Transport Profiles + +In order to have a small client size and generate the optimized client for your use-case Strawberry Shake uses transport profiles. By default Strawberry Shake will generate a client that uses `GraphQL over HTTP` for queries and mutations and `subscriptions-transport-ws` for subscriptions. Meaning if you are only using queries and mutations you need to add the package `StrawberryShake.Transport.Http`. + +There are cases in which we want to define specialize transport profiles where we for instance define for each request type a specific transport. You can define those transport profiles in your `.graphqlrc.json`. + +The following `.graphqlrc.json` can be copied into our getting started example and will create two transport profiles. The first is called `Default` and matches the internal default. It will use `GraphQL over HTTP` by default and use `subscriptions-transport-ws` for subscriptions. The second profile is called `WebSocket` and will also use `GraphQL over HTTP` by default but for mutations and subscriptions it will use `subscriptions-transport-ws`. + +```json +{ + "schema": "schema.graphql", + "documents": "**/*.graphql", + "extensions": { + "strawberryShake": { + "name": "ConferenceClient", + "namespace": "Demo.GraphQL", + "url": "http://localhost:5050/graphql", + "dependencyInjection": true, + "transportProfiles": [ + { + "name": "Default", + "default": "HTTP", + "subscription": "WebSocket" + }, + { + "name": "WebSocket", + "default": "HTTP", + "mutation": "WebSocket", + "subscription": "WebSocket" + } + ] + } + } +} +``` + +The generator will generate the dependency injection code with a new enum called `ConferenceClientProfileKind`. The name of the enum is inferred from your GraphQL client name. The enum can be passed into the dependency injection setup method and allows you to switch between the two transport profiles through configuration. + +```csharp +builder.Services + .AddConferenceClient(profile: ConferenceClientProfileKind.WebSocket) + .ConfigureHttpClient(client => client.BaseAddress = new Uri("http://localhost:5050/graphql")) + .ConfigureWebSocketClient(client => client.Uri = new Uri("ws://localhost:5050/graphql")); +``` diff --git a/website/src/docs/strawberryshake/v12/performance/index.md b/website/src/docs/strawberryshake/v12/performance/index.md new file mode 100644 index 00000000000..28447e86c11 --- /dev/null +++ b/website/src/docs/strawberryshake/v12/performance/index.md @@ -0,0 +1,36 @@ +--- +title: "Performance" +--- + +> We are still working on the documentation for Strawberry Shake so help us by finding typos, missing things or write some additional docs with us. + +In this section we will give you an overview on how to optimize the performance of your GraphQL client. We will put an emphasis on network performance since this is the most critical performance obstacle you are facing when developing fluent applications that also have to work over bad mobile network connections or even when the application becomes fully offline. + +# Persisted queries and automatic persisted queries + +**Improve performance by sending smaller requests and pre-compile queries** + +The size of individual GraphQL requests can become a major pain point. This is not only true for the transport but also introduces inefficiencies for the server since large requests need to be parsed and validated. Hot Chocolate implements for this problem persisted queries. With persisted queries, we can store queries on the server in a key-value store. When we want to execute a persisted query, we can send the key under which the query is stored instead of the query itself. This saves precious bandwidth and also improves execution time since the server will validate, parse, and compile persisted queries just once. + +There are two flavors of persisted queries that Strawberry Shake supports and that is also supported by our GraphQL server Hot Chocolate. + +## Persisted queries + +The first approach is to store queries ahead of time (ahead of deployment). +This is done by extracting the queries from your client application at build time. It will reduce the size of the requests and the bundle size of your application since queries can be completely removed from the client code at build time and are replaced with query hashes. Apart from performance, persisted queries can also be used for security by configuring Hot Chocolate to only accept persisted queries on production environments. + +Read more on how to set up Strawberry Shake for persisted queries [here](/docs/strawberryshake/performance/persisted-queries). + +## Automatic persisted queries + +Automatic persisted queries allow us to store queries dynamically on the server at runtime. With this approach, we can give our application the same performance benefits as with persisted queries without having to opt in to a more complex build process. + +**We are currently still working on this feature so stay tuned on this one.** + +# Persisted State + +Apart from focusing on reducing network request size we also can optimize using the network less by using the stores more efficiently. If you are not yet familiar with the store concepts first head over to [here](/docs/strawberryshake/caching). + +One thing particular here is to persist the state that is aggregated in the stores to either the browsers IndexDB or to some small database like SQLite or LiteDB. When the user leaves the app and later returns to (closes the browser and reopens it) we can fill the state from our storage and have immediately data for the user while our store at the same time will start refreshing that data over the network if that is available. + +Read more on how to set up Strawberry Shake for persisted state [here](/docs/strawberryshake/performance/persisted-state). diff --git a/website/src/docs/strawberryshake/v12/performance/persisted-queries.md b/website/src/docs/strawberryshake/v12/performance/persisted-queries.md new file mode 100644 index 00000000000..6e4bc311f65 --- /dev/null +++ b/website/src/docs/strawberryshake/v12/performance/persisted-queries.md @@ -0,0 +1,39 @@ +--- +title: "Persisted Queries" +--- + +> We are still working on the documentation for Strawberry Shake so help us by finding typos, missing things or write some additional docs with us. + +This guide will walk you through how persisted queries work and how you can set them up with Strawberry Shake. + +# How it works + +Persisted queries is a feature that Facebook uses internally for a long time to improve the performance of Facebook with their relay client. + +During development you can write and edit the queries in your application. When you start and debug your application it will use these queries to interact with the GraphQL server. + +```mermaid +sequenceDiagram + participant Generated Client + participant GraphQL Server + Generated Client->>GraphQL Server: Request: { "query": "{ foo { bar } }" "variables": "..." } + GraphQL Server->>Generated Client: Response: { "data": { "foo": { ... } } } +``` + +Once you package your client application however the GraphQL queries are compiled, removed from the client code and exported into a query directory. + +The query directory can then be uploaded to your GraphQL server. Whenever the client wants to send a GraphQL request to the server it will insert into that request the hash of the extracted GraphQL query instead of the GraphQL query itself. + +```mermaid +sequenceDiagram + participant Generated Client + participant GraphQL Server + Generated Client->>GraphQL Server: Request: { "id": "abc" "variables": "..." } + GraphQL Server->>Generated Client: Response: { "data": { "foo": { ... } } } +``` + +# Setup + +In the following tutorial, we will walk you through creating a Strawberry Shake GraphQL client and configuring it to support persisted queries. + +## Step 1: Create a console project diff --git a/website/src/docs/strawberryshake/v12/performance/persisted-state.md b/website/src/docs/strawberryshake/v12/performance/persisted-state.md new file mode 100644 index 00000000000..95a409677d3 --- /dev/null +++ b/website/src/docs/strawberryshake/v12/performance/persisted-state.md @@ -0,0 +1,7 @@ +--- +title: "Persisted State" +--- + +> We are still working on the documentation for Strawberry Shake so help us by finding typos, missing things or write some additional docs with us. + +Michael is working on it ;) diff --git a/website/src/docs/strawberryshake/v12/scalars.md b/website/src/docs/strawberryshake/v12/scalars.md new file mode 100644 index 00000000000..bffa0a3df2f --- /dev/null +++ b/website/src/docs/strawberryshake/v12/scalars.md @@ -0,0 +1,204 @@ +--- +title: "Scalars" +--- + +Strawberry Shake supports the following scalars out of the box: + +| Type | Description | +| ----------- | ----------------------------------------------------------- | +| `Int` | Signed 32-bit numeric non-fractional value | +| `Float` | Double-precision fractional values as specified by IEEE 754 | +| `String` | UTF-8 character sequences | +| `Boolean` | Boolean type representing true or false | +| `ID` | Unique identifier | +| `Byte` | | +| `ByteArray` | Base64 encoded array of bytes | +| `Short` | Signed 16-bit numeric non-fractional value | +| `Long` | Signed 64-bit numeric non-fractional value | +| `Decimal` | .NET Floating Point Type | +| `Url` | Url | +| `DateTime` | ISO-8601 date time | +| `Date` | ISO-8601 date | +| `Uuid` | GUID | + +# Custom Scalars + +As an addition to the scalars listed above, you can define your own scalars for the client. +A scalar has two representation: the `runtimeType` and the `serializationType`. +The `runtimeType` refers to the type you use in your dotnet application. +The `serializationType` is the type that is used to transport the value. + +Let us explore this with the example of `DateTime`. The server serializes a date into a string on the server. +It is transported as a string over the wire: + +```json +{ + "user": { + // the serializationType in this case is string + "registrationDate": "02-04-2001T12:00:03Z" + } +} +``` + +The `registrationDate` in our .NET client, should on the other hand be represented as a `System.DateTime`. + +```csharp +public partial class GetUser_User : IEquatable, IGetUser_User +{ + // .... + + + // The runtimeType is DateTime + public DateTime? RegistrationDate { get; } + + // .... +} +``` + +By default, all custom scalars are treated like the `String` scalar. +This means, that the client expects a string value and will deserialize it to a `System.String`. + +If you want to change the `serializationType` or/and the `runtimeType` of a scalar, you have to specify the desired types in the `schema.extensions.graphql`. +You can declare a scalar extension and add the `@serializationType` or/and the `@runtimeType` directive. + +```graphql +"Defines the serializationType of a scalar" +directive @serializationType(name: String!) on SCALAR + +"Defines the runtimeType of a scalar" +directive @runtimeType(name: String!) on SCALAR + +"Represents a integer value that is greater or equal to 0" +extend scalar PositiveInt + @serializationType(name: "global::System.Int32") + @runtimeType(name: "global::System.Int32") +``` + +As soon as you specify custom serialization and runtime types you also need to provide a serializer for the type. + +## Serializer + +A scalar identifies its serializer by the scalar name, runtime- and serialization type. +You have to provide an `ISerializer` as soon as you change the `serializationType` or the `runtimeType`. +Use the base class `ScalarSerializer` or `ScalarSerializer` to create you custom serializer. + +### Simple Example + +If the serialization and the value type are identical, you can just use the `ScalarSerializer` base class. + +_schema.extensions.graphql_ + +```graphql +extend scalar PositiveInt + @serializationType(name: "global::System.Int32") + @runtimeType(name: "global::System.Int32") +``` + +_serializer_ + +```csharp +public class PositiveIntSerializer : ScalarSerializer +{ + public PositiveIntSerializer() + : base( + // the name of the scalar + "PositiveInt") + { + } +} +``` + +_configuration_ + +```csharp +serviceCollection.AddSerializer(); +``` + +### Any or JSON + +Some GraphQL schemas contain untyped fields, whose types are often called `Any` or `JSON`. Strawberry Shake allows you to access these fields. + +By default Strawberry Shake will use the built-in `JsonSerializer` to represent these fields as `JsonDocument`. If you want a different representation or use a different JSON library you can do so by providing a custom serializer that handles JSON scalars. + +Json objects are internally handled as `JsonElement` provided by `System.Text.Json`. You can use this to handle serialization by yourself. + +> Note: If you want the raw json from the `JsonElement` use `GetRawText`. +> In order to have a custom serializer you need to specify runtime and serialization type. + +_schema.extensions.graphql_ + +```graphql +extend scalar Any + @serializationType(name: "global::System.Object") + @runtimeType(name: "global::System.Text.Json.JsonElement") +``` + +Also you need to provide a custom serializer to handle the parsing of the `JsonElement` to whatever type you desire. + +_serializer_ + +```csharp +public class MyJsonSerializer : ScalarSerializer +{ + public MyJsonSerializer(string typeName = BuiltInScalarNames.Any) + : base(typeName) + { + } + public override object Parse(JsonElement serializedValue) + { + // handle the serialization of the JsonElement + } + protected override JsonElement Format(object runtimeValue) + { + // handle the serialization of the runtime representation in case + // the scalar is used as a variable. + } +} +``` + +### Advanced Example + +Your schema contains `X509Certificate`'s. These are serialized to `Base64` on the server and transported as strings. + +_schema.extensions.graphql_ + +```graphql +extend scalar X509Certificate + @serializationType(name: "global::System.String") + @runtimeType( + name: "global::System.Security.Cryptography.X509Certificates.X509Certificate2" + ) +``` + +_serializer_ + +```csharp +public class X509CertificateSerializer + : ScalarSerializer +{ + public X509CertificateSerializer() + : base( + // the name of the scalar + "X509Certificate") + { + } + + // Parses the value that is returned from the server (Output) + public override X509Certificate2 Parse(string serializedValue) + { + return new X509Certificate2(Convert.FromBase64String(serializedValue)); + } + + // Formats the value to send to the server (Input) + protected override string Format(X509Certificate2 runtimeValue) + { + return Convert.ToBase64String(runtimeValue.Export(X509ContentType.Cert)); + } +} +``` + +_configuration_ + +```csharp +serviceCollection.AddSerializer(); +``` diff --git a/website/src/docs/strawberryshake/v12/subscriptions.md b/website/src/docs/strawberryshake/v12/subscriptions.md new file mode 100644 index 00000000000..03e1485b045 --- /dev/null +++ b/website/src/docs/strawberryshake/v12/subscriptions.md @@ -0,0 +1,55 @@ +--- +title: "Subscriptions" +--- + +> We are still working on the documentation for Strawberry Shake, so help us by finding typos, missing things, or write some additional docs with us. + +Subscriptions in GraphQL represent real-time events that are represented as a stream of query responses. In most cases, subscriptions are used over WebSockets but can also be used with other protocols. + +> For transport questions, please head over to the [network docs](/docs/strawberryshake/networking). + +GraphQL subscriptions can be used through reactive APIs like queries. Instead of a single network request, the store will subscribe to the GraphQL response stream and update the store for each new result. + +# Setup + +> This section will be based on the [getting started tutorial](/docs/strawberryshake/get-started). + +To create a subscription, we start with everything in Strawberry Shake by creating a GraphQL file. + +1. Create a new GraphQL file and call it `OnSessionUpdated` with the following content. + +```graphql +subscription OnSessionUpdated { + onSessionScheduled { + title + } +} +``` + +2. Add the [StrawberryShake.Transport.WebSockets](https://www.nuget.org/packages/StrawberryShake.Transport.WebSockets) package to your project. + +3. Build your project so that the code-generator kicks in. + +4. Configure the transport settings for the WebSocket. + +```csharp +builder.Services + .AddConferenceClient() + .ConfigureHttpClient(client => client.BaseAddress = new Uri("http://localhost:5050/graphql")) + .ConfigureWebSocketClient(client => client.Uri = new Uri("ws://localhost:5050/graphql")); +``` + +5. A subscription can now be used like any other query by subscribing to it. + +```csharp +var session = + ConferenceClient + .OnSessionUpdated + .Watch() + .Subscribe(result => + { + // do something with the result + }); +``` + +Remember, `session` is an `IDisposable` and will stop receiving events when `Dispose` is invoked. diff --git a/website/src/docs/strawberryshake/v12/tooling.md b/website/src/docs/strawberryshake/v12/tooling.md new file mode 100644 index 00000000000..f2274fb998a --- /dev/null +++ b/website/src/docs/strawberryshake/v12/tooling.md @@ -0,0 +1,66 @@ +--- +title: "Tooling" +--- + +> We are still working on the documentation for Strawberry Shake, so help us by finding typos, missing things, or write some additional docs with us. + +StrawberryShake comes with some tools that integrate into the dotnet CLI and help to setup a project or update the schema of a project. + +# Initialize Project + +`dotnet graphql init http://localhost/graphql` + +The `init` command allows you to initialize a C# project for use with Strawberry Shake. It essentially creates the initial configuration file `.graphqlrc.json` and downloads the GraphQL schema. + +`dotnet graphql init {url} [-p|--Path] [-n|--clientName] [--token] [--scheme] [--tokenEndpoint] [--clientId] [--clientSecret] [--scope] [-x|--headers]` + +| Argument | Description | +| ------------------ | ---------------------------------------------------------------------------------------------------- | +| -p or --path | The path where the project is located. | +| -n or --clientName | The name of the client, which will also become the name of the client class in C#. | +| --token | A token to interact with the server. | +| --scheme | The token schema that shall be used. | +| --tokenEndpoint | The token endpoint that shall be used to acquire a new token. | +| --clientId | The client ID that shall be used when interacting with the `tokenEndpoint`. | +| --clientSecret | The client secret that shall be used when interacting with the `tokenEndpoint`. | +| --scope | The scope (can be used multiple times) that shall be used when interacting with the `tokenEndpoint`. | +| -x or --headers | The headers adds additional custom headers. Example: --headers key1=value1 --headers key2=value2 | + +# Update Project + +`dotnet graphql update` + +The update command allows you to update the local GraphQL schema with the newest version of the GraphQL server. + +`dotnet graphql update [-p|--Path] [-u|--uri] [--token] [--scheme] [--tokenEndpoint] [--clientId] [--clientSecret] [--scope] [-x|--headers]` + +| Argument | Description | +| --------------- | ---------------------------------------------------------------------------------------------------- | +| -p or --path | The path where the project is located. | +| -u or --uri | The GraphQL server URI that shall be used instead of the one in the `graphqlrc.json` | +| --token | A token to interact with the server. | +| --scheme | The token schema that shall be used. | +| --tokenEndpoint | The token endpoint that shall be used to acquire a new token. | +| --clientId | The client ID that shall be used when interacting with the `tokenEndpoint`. | +| --clientSecret | The client secret that shall be used when interacting with the `tokenEndpoint`. | +| --scope | The scope (can be used multiple times) that shall be used when interacting with the `tokenEndpoint`. | +| -x or --headers | The headers adds additional custom headers. Example: --headers key1=value1 --headers key2=value2 | + +# Download Schema + +`dotnet graphql download http://localhost/graphql` + +The download command allows downloading a GraphQL schema from any GraphQL server. + +`dotnet graphql download {url} [-f|--fileName] [--token] [--scheme] [--tokenEndpoint] [--clientId] [--clientSecret] [--scope] [-x|--headers]` + +| Argument | Description | +| ---------------- | ---------------------------------------------------------------------------------------------------- | +| -f or --fileName | The name of the file name. If not specified, the file will be called `Schema.graphql`. | +| --token | A token to interact with the server. | +| --scheme | The token schema that shall be used. | +| --tokenEndpoint | The token endpoint that shall be used to acquire a new token. | +| --clientId | The client ID that shall be used when interacting with the `tokenEndpoint`. | +| --clientSecret | The client secret that shall be used when interacting with the `tokenEndpoint`. | +| --scope | The scope (can be used multiple times) that shall be used when interacting with the `tokenEndpoint`. | +| -x or --headers | The headers adds additional custom headers. Example: --headers key1=value1 --headers key2=value2 |