C#(ASP.NET Core)で GraphQL API を提供する
By TECH PLAY女子部 Advent Calendar 2017 - Qiita
リソースの提供者として GraphQL を使うとき、どうような雰囲気になるのか気になったので調べました。
GraphQL について調べ、GitHub が提供している GraphQL API を触り、C#(ASP.NET Core) で実際に実装してみました。Swagger UI のように、ブラウザ上で実行する環境を ASP.NET Core 上に載せてみました。それらの内容を記載します。
サンプルコード: github.com
GraphQL とは
こちらが参考になりました:GraphQL入門 - 使いたくなるGraphQL - Qiita
GraphQL | A query language for your API は、API のためのクエリ言語(問い合わせ言語)です。Facebook により開発され、2015年にリリースされています。一般的に HTTP 上で使われ、クライアント側は GraphQL でクエリを送信し、サービス側はそれを解釈してデータを返したり更新したりします。
GraphQL API を公開するサービス側は、”どのようなクエリを投げれば、どのようなデータを返すか”を定義します。その定義は、例えば次のようなスキーマで表されます。
type Query { me: User friends: [User] } type User { id: ID name: String }
この例は、me
という名前で問い合わせると User 型のデータが、friends
という名前で問合せると User 型の配列データが、返ることを定義しています。クライアント側はこのスキーマにより、どのようなクエリを書いて送信すればよいかわかります。他の例でいうと、Swagger のスキーマ・OData の metadataと似ています。
このスキーマに基づいて、例えば次のようなクエリを送信すると、下のようなレスポンス(JSON)が返ります。
クエリ:
{ me { name } }
レスポンス(JSON):
{ "me": { "name": "Luke Skywalker" } }
クエリは GraphQL の形式で、データのレスポンスは JSON 形式で記述します。
上のクエリでは、name
フィールドのみ記述しているので、レスポンスの中身はid
の値は無く、name
の値しか格納されていません。同じように、もし User 型が Group 型のフィールドを(クラスの入れ子のように)持つ場合、クエリでレスポンスの中に Group 型を含めるかどうかを決められます。この振る舞いは、Entity Framework の Include や、OData の $select・$expand と似ています。これは GraphQL の特徴の一つで、他にもいろいろあります(そのいろいろを説明しているドキュメントはこちら:Queries and Mutations | GraphQL)。
HTTP 上でのやりとり
このやりとりは実際どのように行われるかというと、多くは HTTP 上で次のように行われます。
※ HTTP 上がほとんどなので、他と区別する必要もないとは思いますが、GraphQL はあくまでもクエリ言語であり、HTTP でどういう風にすべきかは GraphQL の範疇ではありません。かといって、HTTP ではこういうのが一般的だよ、というものもあり、多くのサービスはそれに従っています。これは GraphQL のドキュメントにもあります:Serving over HTTP | GraphQL)
例えば、クライアント側は以下のような HTTP でリクエストを送信します。POST /graphql
で、GraphQL のクエリは、Body 内の JSON 形式のデータのquery
に格納して送信します。
HTTP リクエスト:
POST /graphql HTTP/1.1 Host: api.hogehoge.com Content-Type: application/json { "query" : "query { user { name } }" }
(query { user { name } }
が GraphQL のクエリ言語。)
サービス側は、Body 内の JSON 形式のデータ data
に、クエリに対応するデータを格納します。
HTTP レスポンス:
200 OK Content-Type: application/json { "data": { "user": { "name": "Luke Skywalker" } } }
この例は POST メソッドで送信しましたが、GET メソッドで URL の一部で http://myapi/graphql?query={me{name}}
での送信も行えます(対応しているかはサービスによります。GitHub Graph API は POST のみの対応でした)。
GraphQL API を提供するサービスのエンドポイントは、URL「http://myapi/graphql」のみです。REST API では、URL がリソースを表しているので複数ありますが、GraphQL では、この一個のみです。データの問い合わせ・更新も、Body のクエリに記述して、この URL に対してリクエストを送信します。
スキーマ
また、スキーマをどのように提供するかというと、これも単一のエンドポイントである URL「http://myapi/graphql」で提供します。(参照:Introspection | GraphQL)
スキーマを問い合わせる GraphQL クエリの例:
query { __schema { types { name kind description fields { name } } } }
返ってくるスキーマの例(JSON)GitHub の場合:
{"data":{"__schema":{"queryType":{"name":"Query"},"mutationType":{"name":"Mutation"},"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","description":"Represents `true` or `false` values.","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null}, // 長いので省略
(GitHub特有のオブジェクトは最後の方に定義があります。最初の方は Boolean や String などのシステム型の定義です。)
このスキーマの提供は、他の方法でも行われているようです。例えば GitHub では、GET /graphql
でスキーマのデータを取得できますし、HTML なドキュメントとしても公開しています:Reference | GitHub Developer Guide。また、スキーマをJSONファイルとして別の URL で静的に配置する SDK もあります。
もし、自分がサービス側となる場合は、POST /graphql
のエンドポイントを用意し、GraphQL での問い合わせもこのエンドポイントから取得できるようにする、を基本として、あとはオプションで用意するのが良いと思いました。
改行に注意
HTTP でややこしいのが、GraphQL のクエリは、改行が無いと人には読めないような仕様ですが、HTTP 上では JSON 内の文字列として格納するため、改行を除く(またはエスケープする)必要があるということです。これは Postman や Fiddler などのツールで試す時に困ります。
(Postman で、GitHub GraphQL API にリクエストを送信しているときの図。JSON パースエラーが返ってきている。)
(GraphQL 内の改行はエスケープするか除けば成功する)
※Postman に GraphQL の Issue が立っていました。Add GraphQL support · Issue #1669 · postmanlabs/postman-app-support Insomnia ではサポートされているようです:Introducing GraphQL Support! | Insomnia REST Client
GraphQL を触ってみる
実際に GitHub が提供している GraphQL API を、クライアント側として触ってみます。
GraphiQL で触る
GraphQL には、Swagger UI のように、ブラウザ上でスキーマを閲覧できてリクエストを送信できるツール GraphiQL(名前に i が入ってる)があります。React コンポーネントとして提供されており、HTML/CSS/JavaScript 環境で動作します。
GitHub が提供している場所はこちらです:GraphQL API Explorer | GitHub Developer Guide
右上あたりから GitHub にログインし、▶ボタンで実行でき、レスポンスの中身を確認できます。そのほか、以下のことができます。
- クエリのインテリセンスが効く
- 「Docs」 から、スキーマを確認する
GitHub ではできませんが、最近の GraphiQL だと、クエリの履歴の機能もあります。
スキーマを取得するクエリも試せます:
HTTP で触る
同じように GitHub GraphQL API を HTTP から触ってみます。まず、AccessToken を https://github.com/settings/tokens の 「Generate new token」から取得します。
次のような HTTP リクエストを Postman などのツールで送信します。{acccessToken}
には、取得した Access Token に置き換えます。
HTTP リクエスト
POST /graphql HTTP/1.1 Host: api.github.com Authorization: token {acccessToken} Content-Type: application/json { "query" : "query { viewer { login } }" }
(viewer は Access Token の持ち主の user のこと、login は Login ID みたいなものです。 )
返ってくる HTTP レスポンス
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Server: GitHub.com Status: 200 OK {"data":{"viewer":{"login":"hhyyg"}}}
C# ライブラリ
ここまでで、GraphQL でリソースを提供するには、POST /graphql
エンドポイントで、GraphQL に対応してデータを返すこと、オプションとして GraphiQL でブラウザ上の実行環境を提供することが必要と分かりました。
GraphQL の サーバーサイド、クライアントサイドのライブラリは多くの言語が存在しており、こちらから確認できます Code | GraphQL。よく見かけるのが Apollo GraphQL で、Meteor社が開発しているGraphQLベースのスタックのようです。
Apollo に含まれていませんが、サーバーサイドの C# で人気そうなのは、GraphQL for .NET だったのでこちらで試してみました。ASP.NET や 特定の DB に依存しないので、コンソール上で試すことができます。このほか、IQueryable に変換する ckimes89/graphql-net: Convert GraphQL to IQueryable や、クライアントサイドの bkniffler/graphql-net-client: Very simple GraphQL client for .NET/C# があります。
GraphQL for .NET
.NET Core 2.0 のコンソールプロジェクトを作成し、NuGet「NuGet Gallery | GraphQL 0.17.3」をインストールします。今回は プレリリースのバージョン2.0.0-alpha-811
を使用しました。
はじめは、型を意識せずに、スキーマ・データ・クエリを定義します。
static void My() { ISchema schema = Schema.For(@" type Query { user: String } "); //データを用意 var root = new { User = "Taro" }; string result = schema.Execute(_ => { _.Query = @"{ user }"; _.Root = root; }); Console.WriteLine(result); }
出力:
{ "data": { "user": "Taro" } }
この他にもいろいろ試したコードがこちらにあります。ちなみにこの時点で、スキーマを問い合わせるクエリにも対応しています。
次に、型をつけて、クラスを定義し、データ取得部分も別のクラスのメソッドで行うようにしました。そのコードはこちらになります:Miso.Sample.GraphQLAspNetCore/Program2.cs at master · hhyyg/Miso.Sample.GraphQLAspNetCore
いくつか抜粋します。
オブジェクト Friend の定義: この他に、Droid・Note オブジェクトを定義しています。
public class Friend { public string Id { get; set; } public string Name { get; set; } public string Group { get; set; } } public class FriendType : ObjectGraphType<Friend> { public FriendType() { Field(x => x.Id).Description("The Id"); Field(x => x.Name, nullable: true).Description("The name"); Field(x => x.Group, nullable: true).Description("The group"); } }
Query の定義: GraphQL で取得できる Query を定義しています。
public class StarWarsQuery : ObjectGraphType { public StarWarsQuery() { var myData = new MyData(); // Query "hero" で、Droid クラスのデータを myData.GetDroid() で提供することを表す Field<DroidType>( "hero", resolve: context => myData.GetDroid() ); // Query "note" で、Node クラスのデータを myData.GetNotes() で提供することを表す // 引数は string 型の id である Field<NoteType>( "note", arguments: new QueryArguments( new QueryArgument<NonNullGraphType<StringGraphType>> { Name = "id", Description = "id of the note" } ), resolve: context => myData.GetNotes(context.GetArgument<string>("id")) ); // Query "friends" で、Friend クラスの配列を myData.GetFriends() で提供することを表す // 引数は string 型の group である Field<ListGraphType<FriendType>>( "friends", arguments: new QueryArguments( new QueryArgument<NonNullGraphType<StringGraphType>> { Name = "group", Description = "group val" } ), resolve: context => myData.GetFriends(context.GetArgument<string>("group")) ); } }
ダミーデータを提供する部分:
public class MyData { private List<Droid> _droids = new List<Droid> { new Droid { Id = "123", Name = "R2-D2" }, new Droid { Id = "456", Name = "R45-D456" } }; private List<Note> _notes = new List<Note> { new Note { Id = "1", Text = "no.1 aaaaa"}, new Note { Id = "2", Text = "no.2 bbbbb"}, }; private List<Friend> _friends = new List<Friend> { new Friend { Id = "1", Name = "Jack", Group = "A" }, new Friend { Id = "2", Name = "Siri", Group = "B" }, }; public Droid GetDroid() { return _droids.First(); } public Note GetNotes(string id) { return _notes.SingleOrDefault(x => x.Id == id); } public IEnumerable<Friend> GetFriends(string group) { return _friends.Where(x => x.Group == group); } }
実行部分:
public static void Main(string[] args) { Run().Wait(); } private static async Task Run() { var schema = new Schema { Query = new StarWarsQuery() }; ExecutionResult result = await new DocumentExecuter().ExecuteAsync(_ => { _.Schema = schema; _.Query = @" query { hero { id name } note(id: ""2"") { text } friends(group: ""A"") { id name } } "; }).ConfigureAwait(false); var json = new DocumentWriter(indent: true).Write(result); Console.WriteLine(json);
出力:
{ "data": { "hero": { "id": "123", "name": "R2-D2" }, "note": { "text": "no.2 bbbbb" }, "friends": [ { "id": "1", "name": "Jack" } ] } }
今はダミーデータで静的にデータを取得していますが、もし、外部のデータソースを使用する場合は、StarWarsQuery クラスに対して取得する部分を DI で挿入することもできそうです。
また、このライブラリを使用する場合、オブジェクト毎に、データの取得部分をメソッドで指定しています。もし、クエリで入れ子のリクエストが来た場合—例えば、Friend クラスのフィールドに Friends プロパティを定義している状態で、query friends { name friends { name friends { name friends } } }
というようなクエリを投げた場合は、IEnumerable<Friend> GetFriends(...)
が数回呼ばれることになりそうです。このあたりは、ckimes89/graphql-net: Convert GraphQL to IQueryable や他のライブラリを使用すると、DB に対して効率的にアクセスできる可能性もあるのでしょうか?
ASP.NET Core で提供する
GraphQL の ASP.NET Core のサンプルがありました。JacekKosciesza/StarWars: GraphQL 'Star Wars' example using GraphQL for .NET, ASP.NET Core, Entity Framework Core
こちらは、Entity Framework Core を使っています。また GraphiQL も提供しており、作成手順も README.md に全部載っていて素晴らしいです。
今回作成した私のサンプルは、Entity Framework などは使わず単純なものになっています。またフロント側の GraphiQL をビルドする際の webpack の設定を最新のものにしました。
エンドポイントの用意
GraphQL for .NET ライブラリを使って、ASP.NET Core で、POST /graphql
のエンドポイントを用意します。
ASP.NET Core 2.0 のプロジェクトを作成し、同じように NuGet「NuGet Gallery | GraphQL 0.17.3」をインストールします。バージョン2.0.0-alpha-811
です。
POST で送信する Body の内容を、次のようにクラスで定義します。
namespace GraphQLSampleWeb.GraphQL { public class GraphQLQuery { public string OperationName { get; set; } public string NamedQuery { get; set; } public string Query { get; set; } public string Variables { get; set; } } }
POST graphql
を公開する Controller を作成します:
using GraphQL; using GraphQL.Types; using GraphQLSampleWeb.GraphQL; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; namespace GraphQLSampleWeb.Controllers { [Produces("application/json")] [Route("graphql")] public class GraphQLController : Controller { [HttpPost] public async Task<IActionResult> Post([FromBody] GraphQLQuery query) { var schema = Schema.For(@" type Query { user: Person } type Person { id: String name: String } "); var root = new { User = new { Id = "1", Name = "Taro" } }; var result = await new DocumentExecuter().ExecuteAsync(_ => { _.Schema = schema; _.Query = query.Query; _.Root = root; }).ConfigureAwait(false); return Ok(result); } } }
デバッグ実行し、次の HTTP リクエストを投げると、データが返ってきます。
HTTP リクエスト:
POST /graphql HTTP/1.1 Host: localhost:58764 Content-Type: application/json Cache-Control: no-cache Postman-Token: e4a3fa08-c8d9-af46-7a39-e4f3e55c6324 { "query": "query { user { id name }}" }
HTTP レスポンスの Body
{ "data": { "user": { "id": "1", "name": "Taro" } } }
GraphiQL の用意
GraphiQL の実行ページは/index.html
で提供することにします。そのため、ASP.NET Core で静的ファイルを有効にしておきます。
Startup.cs の Configure のところにapp.UseStaticFiles()
を追加:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseStaticFiles();
こちらを参考に以下の作業を行います。 Miso.Sample.GraphQLAspNetCore/GraphQLSampleWeb at master · hhyyg/Miso.Sample.GraphQLAspNetCore
- index.html をコピー
- Scripts/app.js ファイルをコピー
- package.json ファイルをコピーし、
npm install
oryarn install
でパッケージをインストール - webpack.config.js ファイルをコピーし、
yarn webpack
を実行- この設定により webpack でビルドすると、
wwwroot
フォルダ内にbundle.js
graphiql.css
index.html
が配置されます。 - index.html はそのまま wwwroot 配下にコピーするように設定しています。
- graphiql に必要な JS と、Scripts/app.js をまとめて
bundle.js
を作成し、wwwroot 配下に配置するよう設定しています。 graphiql.css
は、./node_modules/graphiql/graphiql.css
を wwwroot 配下にコピーするよう設定しています。
- この設定により webpack でビルドすると、
これらの手順はそのうち古くなると思いますので、ドキュメント graphql/graphiql: An in-browser IDE for exploring GraphQL. の README.md や example を参考にします。
yarn webpack
が成功したあと、ASP.NET Core のプロジェクトをデバッグ実行し、index.html
にアクセスすると、GraphiQL を試せます。
アクセスした時に HTTP をキャプチャすると、GraphiQL によって /graphql
にスキーマを問い合わせるリクエストが送信されていることを確認できます。