GraphQL Client with Strawberry Shake

使用 Strawberry Shake v13.81 實作 GraphQL Client。

引言

在 .NET Core 有多種實作 GraphQL 方案。本人選取 ChilliCream GraphQL PlatformHot Chocolate v13 實作 Server 端;Strawberry Shake v13 實作 Client 端。

實作前需知

GraphQL 發展過程已過數個迭代。就 Hot Chocolate 來說至少有主要三種世代的開發模式:Schema-first 、Code-first、Annotation-based。我們只採用最新的開發模式:Annotation-based。

開發環境

平台: .NET6

IDE: Visual Studio 2022

框架: Blazor Server App

Strawberry Shake 官方教學

官方教學已從入門開始,就不再手把手教學。

strawberry shake - tutorial
strawberry shake - data types

Strawberry Shake 關鍵程式碼紀錄

官方教學已從入門開始,就不再手把手教學。以下只紀錄重要的關鍵的部份。

Strawberry Shake - Client package

Strawberry Shake 的 Client package 分三種:BlazorXamarinConsole 。我們選用 Console 模式,因為框架是 Blazor Server App。經完成練習後應該也可以選用 Blazor 模式。

Strawberry Shake CLI tools

不管選用那種 client 模式都要安裝Strawberry Shake CLI tools: StrawberryShake.Tools

> dotnet new tool-manifest
> dotnet tool install StrawberryShake.Tools --local

安裝成功後在 dotnet-tools.json 組態檔會加入一筆 "strawberryshake.tools" 設定。

~\.config\dotnet-tools.json
{
  "version": 1,
  "isRoot": true,
  "tools": {
    "strawberryshake.tools": {
      "version": "13.8.1",
      "commands": [
        "dotnet-graphql"
      ]
    }
  }
}

基於 GraphQL 是封包規格導向,與 Swagger 、gRPC、SOAP/Web Service 等一樣可以用工具生成相應的 context/data model 。只要服務端有異動可以用此 CLI 工具立即同步 Strawberry Shake 組態檔。

指令如下:

指令語法: dotnet graphql init {{ServerUrl}} -n {{ClientName}} -p {{TargetFolder}} 
> dotnet graphql init https://localhost:5000/graphql/ -n MyGraphClient -p ./

Strawberry Shake | GraphQL Client 組態檔

strawberryshake.tools 將依副檔名為 *.graphql 的檔案自動生成相應的 context/data model 。只要成功產生就能輕鬆與 GraphQL Server 通訊。

依 Strawberry Shake CLI tools 會有下列檔案:

  • .graphqlrc.json 組態檔主檔。建議調整"name", "spacename",並確認 "url" 是正確的。

  • schema.graphql 該 GraphQL Server Schema 的 SDL 描述。自動生成不用改它。

  • schema.extensions.graphql

    延伸參數。自動生成不用改它。

  • MyGraphClient.graphql 由工具生成,可以調整合適的檔名。 放置 GraphQL client 的 SDL 描述檔。 將依此檔產生 client side context code。

Program.cs -註冊 GraphQL client

註冊 GraphQL client。其中若有 subscription 需求就要增加 ConfigureWebSocketClient 設定啟用 web socket 通訊。

Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
//## from GraphQL client
builder.Services
       .AddN6GraphQLabClient() // 該 GraphQL client 名稱由 .graphqlrc.json 決定。
       .ConfigureHttpClient(cfg => 
              cfg.BaseAddress = new Uri("https://localhost:5000/graphql/"))
       .ConfigureWebSocketClient(cfg => 
              cfg.Uri = new Uri("wss://localhost:5000/graphql/")); 
             // wss := WebSocket + SSL, for subscription
             // ws := WebSocket。

[...略...]
var app = builder.Build();
[...略...]

下面是 GraphQL client 端 SDL 描述檔。將會被 strawberryshake.tools用來生成 client side code。

MyGraphClient.graphql
# 無查詢參數 query
query GetBookList {
  bookList {
    title
  }
}

# 有查詢參數 query
query GetBook($title:String!) {
  book(title: $title) {
    title,
    author {
      name
    }
  }
}

# 訂閱
subscription ListenTimerTick {
  onTimerTick {
    tick
  }
}

# 異動
mutation addProdcut($input:ProductInput!) {
  addProdcut(input: $input) {
    isSuccess
    newProduct {
      id,
      name,
      quantity
    }
  }
}

# 訂閱異動
subscription ListenProductAdded {
  onProductAdded {
    isSuccess,
    newProduct {
      id,
      name,
      quantity
    }
  }
}

關鍵原始碼

其中 IGraphQ Client API 類別名稱與命名空間由 .graphqlrc.json 組態檔決定。

query client 範例

@using N6GraphClient.GraphQL
@inject IN6GraphQLabClient glc // graphql client

[...略...]

@code {
  //## State
  string message = string.Empty;
  IGetBook_Book? theBook = null;

  async Task HandleClick()
  {
    // 無參數 query
    var result2 = await glc.GetBookList.ExecuteAsync();
    foreach (var item in result2.Data!.BookList)
      message += item.Title + "; ";

    // 有參數 query
    var result3 = await glc.GetBook.ExecuteAsync("SOAP");
    theBook = result3.Data?.Book;    
    message += " 作者之一: " + (theBook?.Author?.Name ?? "nil");
  }
}

mutation client 範例

@using N6GraphClient.GraphQL
@inject IN6GraphQLabClient glc

[...略...]

@code {
  //## State
  IAddProdcut_AddProdcut? addProdcutPayload = null;

  async Task AddProduct()
  {
    var input = new ProductInput()
    {
      Name = "超大型書櫃",
      Quantity = 5
    };
	
    var result = await glc.AddProdcut.ExecuteAsync(input);
    addProdcutPayload = result.Data!.AddProdcut;
  }
}

subscription client

【啟動訂閱】後會拿到連線 session 物件。只要 dispose 該 session 就會【取消訂閱】。

MySubscriptionWidget.razor
@using N6GraphClient.GraphQL
@using StrawberryShake.Extensions
@implements IDisposable
@inject IN6GraphQLabClient glc

<div style="border:solid 2px red; padding:8px; margin-bottom:8px;">
  <h3>MySubscriptionWidget</h3>

  @if(session != null)
  {
    <p>listen...</p>    
  }

  @if (receivedMessage != null)
  {
  <pre>
      @System.Text.Json.JsonSerializer.Serialize(receivedMessage, jsonOptions)
  </pre>
  }

  <button @onclick=HandleSubscribe>啟動訂閱</button>
  <button @onclick=CancelSubscribe>取消訂閱</button>
</div>

@code {
  //## Resource
  System.Text.Json.JsonSerializerOptions jsonOptions = new System.Text.Json.JsonSerializerOptions
  {
    WriteIndented = true,
    Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
  };

  //## State
  IDisposable? session = null;
  IListenProductAdded_OnProductAdded? receivedMessage = null;

  void IDisposable.Dispose()
  {
    session?.Dispose();
    session = null;
  }

  /// <summary>
  /// 啟動訂閱
  /// </summary>
  Task HandleSubscribe()
  {
    session = glc.ListenProductAdded
            .Watch()
            .Subscribe(async result =>
            {
              // do something with the result
              receivedMessage = result.Data!.OnProductAdded;
              await InvokeAsync(StateHasChanged);
            });

    return Task.CompletedTask;
  }

  /// <summary>
  /// 取消訂閱
  /// </summary>
  Task CancelSubscribe()
  {
    session?.Dispose();
    session = null;
    return Task.CompletedTask;
  }
}

完整程式碼

(EOF)

Last updated