Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

.NET GraphQL Client のリアル

.NET GraphQL Client のリアル

■イベント
イマドキのC# .NET Web開発 〜gRPC, GraphQL, Blazorもあるよ〜
https://sansan.connpass.com/event/316664/

■発表者
Sansan Engineering Unit Data Hubグループ
⽊下 賢也

■ Sansan Data Hub エンジニア採用情報
https://media.sansan-engineering.com/datahub-engineer

SansanTech

May 15, 2024
Tweet

More Decks by SansanTech

Other Decks in Technology

Transcript

  1. 2 © Sansan, Inc. ⾃⼰紹介 2021年に新卒でSansanに⼊社 ⼊社当初からSansan Data Hubの開発メンバーとして従事 している。

    最近はプロダクトのオブザーバビリティーを⾼めることに 向き合っている。その中でもSQL Server, Elasticsearch, Cosmosなどインフラの内部構造に興味があり、積極的に 学んでいる。 ⽊下 賢也(Kinoshita Kenya)
  2. 3 © Sansan, Inc. ※詳細は省略しています (各マイクロサービスのデータストア等) 本発表のスコープ 管理⽤画⾯ エンリッチ⽤ データソース

    データ書き出し先 データ取り込み元 データ連携⽤API エンリッチ処理群 書き出し処理群 取り込み処理群 コアデータ群 GraphQL の部分を紹介 .NET GraphQL Client のリアル Blazor WASM x Code First gRPCで始める C# ⼤統⼀理論
  3. 4 © Sansan, Inc. 本発表のスコープ - GraphQL Client を使ってみてのお話 話すこと

    話さないこと - GraphQL の詳細なお話 - GraphQL Server のお話
  4. 5 © Sansan, Inc. アジェンダ 1. Sansan Data Hub でのエンリッチ処理

    2. GraphQL とは 3. .NET で GraphQL Client ライブラリ 4. Strawberry Shake の特徴 5. Client 側の実装 6. 導⼊してみて
  5. 6 © Sansan, Inc. Sansan Data Hub でのエンリッチ処理 - エンリッチ処理とは

    - 統合した拠点や組織に対し、帝国データバンク情報などを付与する事 - エンリッチ⽤のデータソースは社内の別チームが管理している - Data Hub はそれを API で受け取っている エンリッチ処理群 Data Hubチーム エンリッチ⽤ データソース 別チームの管轄 REST API API A API C API B API D
  6. 7 © Sansan, Inc. GraphQL とは - REST と⽐較して -

    クライアントが必要とする項⽬だけ取得出来る > オーバーフェッチを減らせる - 複数リソースを必要としても API リクエストを⼀回に出来る > アンダーフェッチを減らせる - エンドポイントが⼀つで POST のみ > クライアントで欲しいリソースが増えても、クエリで表現出来る > キャッシュどうしようか - エラーが起きても HTTP ステータスコードは 200 > 変わりにレスポンスボディの errors で表現する
  7. 8 © Sansan, Inc. .NET での GraphQL Strawberry Shake (GraphQL

    Client) GraphQL.Client - スキーマからのコード⽣成あり - ⽐較的複雑な作りになっている - CLI あり - スキーマからのコード⽣成なし - シンプルな作りなので、取っかかり やすい - CLI なし
  8. 9 © Sansan, Inc. .NET での GraphQL Strawberry Shake (GraphQL

    Client) GraphQL.Client - スキーマからのコード⽣成あり - ⽐較的複雑な作りになっている - CLI あり - スキーマからのコード⽣成なし - シンプルな作りなので、取っかかり やすい - CLI なし 採⽤
  9. 10 © Sansan, Inc. Strawberry Shake の特徴 - GraphQL Server

    からスキーマを取ってきて更新してくれる - CLI でできる - Relay Connection にも対応しているよ - GraphQL スキーマから Roslyn API を使って以下のコード⽣成 - GraphQL ⽤ API Client - Operation を表した拡張メソッド - GraphQL Server からのレスポンスモデル - ServiceCollection への 追加 - …. - Query が Schema に準拠していない時ビルド時に弾いてくれる
  10. 11 © Sansan, Inc. スキーマ、クエリ例 「拠点情報」のスキーマ """拠点情報""" type BusinessLocation implements

    Node { """The ID of an object""" id: ID! organizationId: String! businessLocationId: String! """拠点名""" name: String! """住所""" address: String! } """An object with an ID""" interface Node { """The id of the object.""" id: ID! } 「拠点情報の⼀覧を取得」をするクエリ query GetBusinessLocationsByOrganizationId($organizationId: String!) { organization(organizationId: $organizationId) { organizationId businessLocations{ edges { node { organizationId businessLocationId name address } } } } }
  11. 12 © Sansan, Inc. スキーマ、クエリ例 「拠点情報」のスキーマ """拠点情報""" type BusinessLocation implements

    Node { """The ID of an object""" id: ID! organizationId: String! businessLocationId: String! """拠点名""" name: String! """住所""" address: String! } """An object with an ID""" interface Node { """The id of the object.""" id: ID! } 「拠点情報の⼀覧を取得」をするクエリ query GetBusinessLocationsByOrganizationId($organizationId: String!) { organization(organizationId: $organizationId) { organizationId businessLocations{ edges { node { organizationId businessLocationId name address } } } } }
  12. 13 © Sansan, Inc. コード例: Query の実⾏ 「拠点情報⼀覧取得」クエリの実⾏ var result

    = await _apiClient.GetBusinessLocationsByOrganizationId.ExecuteAsync(organizationId, cancellationToken); if (result.IsErrorResult()) return new ApiClient.Models.Response<IReadOnlyList<BusinessLocation>>(result.Errors); var organization = result.Data?.Organization; if (organization is null) return new ApiClient.Models.Response<IReadOnlyList<BusinessLocation>>(new List<BusinessLocation>()); return new ApiClient.Models.Response<IReadOnlyList<BusinessLocation>>(organization.BusinessLocations.Edges.Select(x => { var n = x.Node; return new BusinessLocation { BusinessLocationId = n.BusinessLocationId, OrganizationId = n.OrganizationId, Name = n.Name, Address = n.Address }; }).ToList());
  13. 14 © Sansan, Inc. コード例: クライアントでのエラーハンドリング 「拠点情報⼀覧取得」クエリの実⾏ var result =

    await _apiClient.GetBusinessLocationsByOrganizationId.ExecuteAsync(organizationId, cancellationToken); if (result.IsErrorResult()) return new ApiClient.Models.Response<IReadOnlyList<BusinessLocation>>(result.Errors); var organization = result.Data?.Organization; if (organization is null) return new ApiClient.Models.Response<IReadOnlyList<BusinessLocation>>(new List<BusinessLocation>()); return new ApiClient.Models.Response<IReadOnlyList<BusinessLocation>>(organization.BusinessLocations.Edges.Select(x => { var n = x.Node; return new BusinessLocation { BusinessLocationId = n.BusinessLocationId, OrganizationId = n.OrganizationId, Name = n.Name, Address = n.Address }; }).ToList());
  14. 15 © Sansan, Inc. コード例: グラフの表現 「拠点情報⼀覧取得」クエリの実⾏ var result =

    await _apiClient.GetBusinessLocationsByOrganizationId.ExecuteAsync(organizationId, cancellationToken); if (result.IsErrorResult()) return new ApiClient.Models.Response<IReadOnlyList<BusinessLocation>>(result.Errors); var organization = result.Data?.Organization; if (organization is null) return new ApiClient.Models.Response<IReadOnlyList<BusinessLocation>>(new List<BusinessLocation>()); return new ApiClient.Models.Response<IReadOnlyList<BusinessLocation>>(organization.BusinessLocations.Edges.Select(x => { var n = x.Node; return new BusinessLocation { BusinessLocationId = n.BusinessLocationId, OrganizationId = n.OrganizationId, Name = n.Name, Address = n.Address }; }).ToList());
  15. 16 © Sansan, Inc. コード例: 単体テスト 「拠点情報⼀覧取得」単体テスト⽤モック var response =

    new GetBusinessLocationsByOrganizationId_Organization_Organization(“organization_id”, new GetBusinessLocationsBySoc_Organization_BusinessLocations_BusinessLocationConnection( new List<IGetBusinessLocationsBySoc_Organization_BusinessLocations_Edges> { new GetBusinessLocationsBySoc_Organization_BusinessLocations_Edges_BusinessLocationEdge( new GetBusinessLocationsByOrganizationId_Organization_BusinessLocations_Edges_Node_BusinessLocation(“organization _id”, "business_location_id", "business_location_name", “test_address”))), })); var mockApiClient = new Mock<IApiClient>(); var mockResponse = new Mock<IOperationResult<IGetBusinessLocationsByOrganizationIdResult>>(); mockResponse.Setup(x => x.Errors).Returns(Array.Empty<IClientError>()); mockResponse.Setup(x => x.Data).Returns( new GetBusinessLocationsBySocResult(response)); mockApiClient .Setup(x => x.GetBusinessLocationsByOrganizationId.ExecuteAsync(“organization_id”, It.IsAny<CancellationToken>())) .ReturnsAsync(mockResponse.Object);
  16. 17 © Sansan, Inc. コード例: 単体テスト 「拠点情報⼀覧取得」単体テスト⽤モック var response =

    new GetBusinessLocationsByOrganizationId_Organization_Organization(“organization_id”, new GetBusinessLocationsBySoc_Organization_BusinessLocations_BusinessLocationConnection( new List<IGetBusinessLocationsBySoc_Organization_BusinessLocations_Edges> { new GetBusinessLocationsBySoc_Organization_BusinessLocations_Edges_BusinessLocationEdge( new GetBusinessLocationsByOrganizationId_Organization_BusinessLocations_Edges_Node_BusinessLocation(“organization _id”, "business_location_id", "business_location_name", “test_address”))), })); var mockApiClient = new Mock<IApiClient>(); var mockResponse = new Mock<IOperationResult<IGetBusinessLocationsByOrganizationIdResult>>(); mockResponse.Setup(x => x.Errors).Returns(Array.Empty<IClientError>()); mockResponse.Setup(x => x.Data).Returns( new GetBusinessLocationsBySocResult(response)); mockApiClient .Setup(x => x.GetBusinessLocationsByOrganizationId.ExecuteAsync(“organization_id”, It.IsAny<CancellationToken>())) .ReturnsAsync(mockResponse.Object);
  17. 18 © Sansan, Inc. おまけ: その他⽣成されるコード クエリの持ち⽅ public partial class

    GetBusinessLocationsByOrganizationIdQueryDocument : global::StrawberryShake.IDocument { private GetBusinessLocationsBySocQueryDocument() { } public static GetBusinessLocationsByOrganizationIdQueryDocument Instance { get; } = new GetBusinessLocationsBySocQueryDocument(); public global::StrawberryShake.OperationKind Kind => global::StrawberryShake.OperationKind.Query; public global::System.ReadOnlySpan<global::System.Byte> Body => new global::System.Byte[]{0x71, 0x75, 0x65,...}; - Request Body は ReadOnlySpan に変換 - GraphQLのクエリのゼロアロケーション最適化が有効になるような コードが⽣成される - のに、Query する際に string へ変換
  18. 19 © Sansan, Inc. [まとめ] StrawberryShakeを導⼊してみた所感 - ポジティブ - ライブラリでやってくれることが多く、コードの書き⼼地もよい

    - GraphQL を意識する事が少ない - 同じ仕組みに乗っかって機能を追加していけそう - ネガティブ - GraphQL の概念に慣れるまでは⾟い - かもしれない