Slide 1

Slide 1 text

.NET GraphQL Client のリアル Sansan株式会社 ⽊下賢也

Slide 2

Slide 2 text

2 © Sansan, Inc. ⾃⼰紹介 2021年に新卒でSansanに⼊社 ⼊社当初からSansan Data Hubの開発メンバーとして従事 している。 最近はプロダクトのオブザーバビリティーを⾼めることに 向き合っている。その中でもSQL Server, Elasticsearch, Cosmosなどインフラの内部構造に興味があり、積極的に 学んでいる。 ⽊下 賢也(Kinoshita Kenya)

Slide 3

Slide 3 text

3 © Sansan, Inc. ※詳細は省略しています (各マイクロサービスのデータストア等) 本発表のスコープ 管理⽤画⾯ エンリッチ⽤ データソース データ書き出し先 データ取り込み元 データ連携⽤API エンリッチ処理群 書き出し処理群 取り込み処理群 コアデータ群 GraphQL の部分を紹介 .NET GraphQL Client のリアル Blazor WASM x Code First gRPCで始める C# ⼤統⼀理論

Slide 4

Slide 4 text

4 © Sansan, Inc. 本発表のスコープ - GraphQL Client を使ってみてのお話 話すこと 話さないこと - GraphQL の詳細なお話 - GraphQL Server のお話

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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 } } } } }

Slide 12

Slide 12 text

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 } } } } }

Slide 13

Slide 13 text

13 © Sansan, Inc. コード例: Query の実⾏ 「拠点情報⼀覧取得」クエリの実⾏ var result = await _apiClient.GetBusinessLocationsByOrganizationId.ExecuteAsync(organizationId, cancellationToken); if (result.IsErrorResult()) return new ApiClient.Models.Response>(result.Errors); var organization = result.Data?.Organization; if (organization is null) return new ApiClient.Models.Response>(new List()); return new ApiClient.Models.Response>(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());

Slide 14

Slide 14 text

14 © Sansan, Inc. コード例: クライアントでのエラーハンドリング 「拠点情報⼀覧取得」クエリの実⾏ var result = await _apiClient.GetBusinessLocationsByOrganizationId.ExecuteAsync(organizationId, cancellationToken); if (result.IsErrorResult()) return new ApiClient.Models.Response>(result.Errors); var organization = result.Data?.Organization; if (organization is null) return new ApiClient.Models.Response>(new List()); return new ApiClient.Models.Response>(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());

Slide 15

Slide 15 text

15 © Sansan, Inc. コード例: グラフの表現 「拠点情報⼀覧取得」クエリの実⾏ var result = await _apiClient.GetBusinessLocationsByOrganizationId.ExecuteAsync(organizationId, cancellationToken); if (result.IsErrorResult()) return new ApiClient.Models.Response>(result.Errors); var organization = result.Data?.Organization; if (organization is null) return new ApiClient.Models.Response>(new List()); return new ApiClient.Models.Response>(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());

Slide 16

Slide 16 text

16 © Sansan, Inc. コード例: 単体テスト 「拠点情報⼀覧取得」単体テスト⽤モック var response = new GetBusinessLocationsByOrganizationId_Organization_Organization(“organization_id”, new GetBusinessLocationsBySoc_Organization_BusinessLocations_BusinessLocationConnection( new List { 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(); var mockResponse = new Mock>(); mockResponse.Setup(x => x.Errors).Returns(Array.Empty()); mockResponse.Setup(x => x.Data).Returns( new GetBusinessLocationsBySocResult(response)); mockApiClient .Setup(x => x.GetBusinessLocationsByOrganizationId.ExecuteAsync(“organization_id”, It.IsAny())) .ReturnsAsync(mockResponse.Object);

Slide 17

Slide 17 text

17 © Sansan, Inc. コード例: 単体テスト 「拠点情報⼀覧取得」単体テスト⽤モック var response = new GetBusinessLocationsByOrganizationId_Organization_Organization(“organization_id”, new GetBusinessLocationsBySoc_Organization_BusinessLocations_BusinessLocationConnection( new List { 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(); var mockResponse = new Mock>(); mockResponse.Setup(x => x.Errors).Returns(Array.Empty()); mockResponse.Setup(x => x.Data).Returns( new GetBusinessLocationsBySocResult(response)); mockApiClient .Setup(x => x.GetBusinessLocationsByOrganizationId.ExecuteAsync(“organization_id”, It.IsAny())) .ReturnsAsync(mockResponse.Object);

Slide 18

Slide 18 text

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 Body => new global::System.Byte[]{0x71, 0x75, 0x65,...}; - Request Body は ReadOnlySpan に変換 - GraphQLのクエリのゼロアロケーション最適化が有効になるような コードが⽣成される - のに、Query する際に string へ変換

Slide 19

Slide 19 text

19 © Sansan, Inc. [まとめ] StrawberryShakeを導⼊してみた所感 - ポジティブ - ライブラリでやってくれることが多く、コードの書き⼼地もよい - GraphQL を意識する事が少ない - 同じ仕組みに乗っかって機能を追加していけそう - ネガティブ - GraphQL の概念に慣れるまでは⾟い - かもしれない

Slide 20

Slide 20 text

No content