GraphQL Client with Strawberry Shake
使用 Strawberry Shake v13.81 實作 GraphQL Client。
引言
在 .NET Core 有多種實作 GraphQL 方案。本人選取 ChilliCream GraphQL Platform 的 Hot 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 關鍵程式碼紀錄
官方教學已從入門開始,就不再手把手教學。以下只紀錄重要的關鍵的部份。
Strawberry Shake - Client package
Strawberry Shake 的 Client package 分三種:Blazor、XamarinConsole 。我們選用 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" 設定。
{
"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 通訊。
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。
# 無查詢參數 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 就會【取消訂閱】。
@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