miso_soup3 Blog

主に ASP.NET 関連について書いています。

Azure Functions で1ページを cshtml で出力したい

cshtml を1つだけホストしたいけど、そのために ASP.NET と PaaS は重いなと思い、Azure Functions でレンダリングするようにしました。

下記のような条件を満たす Azure Functions を作成します。

  • Azure Functions で、1ページだけ HTML を出力したい
  • 1ページだけだけど、クエリ文字列やフォーム送信のデータは、取得して C# で処理したい
  • Razor(cshtml)で書きたい

今回作成したページの図です:

f:id:miso_soup3:20171201202116p:plain

用途としては、アクセスが少ない適当なサンプルページを公開したいものとします。

別件ですが、cshtml ではなく静的な html をホストしたい場合は、こちら Serving an HTML Page from Azure Functions を参考にします。

サンプルコード

github.com

概要

Azure Functions で、HTTP Trigger の Function を作成します。プロジェクト配下には cshtml ファイルを配置し、それを読み取り、Razor コンパイルを行って、text/html 形式で HTTP レスポンスを返します。

Razor でコンパイルする方法ですが、RazorEngine では駄目そうだったので(参照:Error in Azure Function · Issue #496 · Antaris/RazorEngine)、Westwind.RazorHosting パッケージを使用します。

手順

  • Visual Studio を使用して Azure で初めての関数を作成する | Microsoft Docs を参考し、C# プリコンパイル形式の Azure Function のプロジェクトを作成します。
  • NuGet で、NuGet Gallery | Westwind.RazorHosting をインストールします。(現在は version 3.1.0)
  • NuGet「Microsoft.NET.Sdk.Functions」を最新版にアップデートします。
  • プロジェクト配下に、「templates」フォルダを作成します。
  • 「templates」フォルダの下に、「Page.cshtml」ファイルを作成します。 「Page.cshtml」のプロパティ設定にて、「出力ディレクトリにコピー」の値を「新しい場合はコピーにする」に設定します。
  • HTTP Trigger の関数「Function1.cs」を、アクセス権は Anonymous にして作成します(参考:関数を作成する)。

作成後は、このようなソリューション構成になります。

f:id:miso_soup3:20171201204154p:plain

  • 「Page.cshtml」と「Function1.cs」の中身を、次のように記述します。

Page.cshtml:

<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Sample</title>
</head>

<body>
    <h1>Sample</h1>

    <div>NameInQuery:@Model.NameInQuery</div>
    <div>NameInBody:@Model.NameInBody</div>

    <form method="get">
        name: <input type="text" value="" name="name" />
        <input type="submit" value="GET" />
    </form>

    <form method="post">
        name: <input type="text" value="" name="name" />
        <input type="submit" value="POST" />
    </form>
</body>
</html>

Function1.cs:

using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Westwind.RazorHosting;

namespace Miso.SampleRazorFunctions
{
    public static class Function1
    {
        private static string compiledId = "";
        private static RazorEngine<RazorTemplateBase> host = new RazorEngine<RazorTemplateBase>();

        [FunctionName(nameof(Function1))]
        public static async Task<HttpResponseMessage> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]HttpRequestMessage req, 
            TraceWriter log,
            ExecutionContext context)
        {
            //.dllを追加する場合
            //host.AddAssembly(Path.Combine(context.FunctionAppDirectory, $@"bin\System.ValueTuple.dll"));
            
            if (string.IsNullOrEmpty(compiledId))
            {
                compiledId = await Helper.GetCompileId(host, context, "Page.cshtml");
            }

            log.Info($"compileId:{compiledId}");

            if (!string.IsNullOrEmpty(host.ErrorMessage))
            {
                log.Error(host.ErrorMessage);
                return req.CreateResponse(HttpStatusCode.InternalServerError, host.ErrorMessage);
            }

            // parse query parameter
            string nameInQueryParameter = req.GetQueryNameValuePairs()
                .FirstOrDefault(q => string.Compare(q.Key, "name", true) == 0)
                .Value;

            // Get request body
            var formData = await req.Content.ReadAsFormDataAsync();
            var nameInBody = formData == null ? "" : formData["name"];

            var model = new Data()
            {
                NameInQuery = nameInQueryParameter,
                NameInBody = nameInBody,
            };

            string result = host.RenderTemplateFromAssembly(compiledId, model);

            return new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent(result, Encoding.UTF8, "text/html")
            };
        }
    }

    public class Data
    {
        public string NameInQuery { get; set; }
        public string NameInBody { get; set; }
    }

    public class Helper
    {
        public static async Task<string> GetCompileId(RazorEngine<RazorTemplateBase> host, ExecutionContext context, string csHtmlFileName)
        {
            string template;
            string templatePath = Path.Combine(context.FunctionAppDirectory, $@"templates\{csHtmlFileName}");
            using (var reader = File.OpenText(templatePath))
            {
                template = await reader.ReadToEndAsync();
            }
            return host.CompileTemplate(template);
        }
    }
}

以上で、デバッグ実行し、URL にアクセスすると(例:http://localhost:7071/api/Function1)、レンダリングされた HTML を表示できます。

ノウハウなど

この方法では、通常の Visual Studio における ASP.NET 開発とは違い、.cshtml にエラーがある場合(Razor コンパイルでエラーが発生する場合)は、実行してみないと内容が分かりません。ですので、エラーの原因を調査するのに少々手間がかかります。もし、エラーが発生している場合は、host.CompileTemplate(template) を実行した時点で、host オブジェクト内にエラーメッセージが格納されます。上記のサンプルコードでは、エラーが発生した場合はその内容を HTTP レスポンスで返すようにしています。

.cshtml において、NuGet で取得してきた .dll などを参照している場合は、以下のようにアセンブリの追加を宣言します(上記のコードにも記述しています)。

host.AddAssembly(Path.Combine(context.FunctionAppDirectory, $@"bin\System.ValueTuple.dll"));`

同じように、namespace を追加する場合は、このように。

host.AddNamespace("Microsoft.Azure.WebJobs.Host")

必要な .dll が存在するかどうかを確認するときは、プロジェクトを Debug ビルドした後 \ソリューション名\bin\Debug\net462\bin 内を見てみます。

コンパイルの後(host.CompileTemplate(template);実行後)に、host 変数内から、アセンブリ情報や namespace 情報を確認できます。その他 Razor コンパイルについては、パッケージの README.md を参照します。

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 などのツールで試す時に困ります。

f:id:miso_soup3:20171130135556p:plain

(Postman で、GitHub GraphQL API にリクエストを送信しているときの図。JSON パースエラーが返ってきている。)

f:id:miso_soup3:20171130135729p:plain

(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 だと、クエリの履歴の機能もあります。

f:id:miso_soup3:20171130143146p:plain

スキーマを取得するクエリも試せます: f:id:miso_soup3:20171130143900p:plain

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 or yarn 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 配下にコピーするよう設定しています。

これらの手順はそのうち古くなると思いますので、ドキュメント graphql/graphiql: An in-browser IDE for exploring GraphQL. の README.md や example を参考にします。

yarn webpack が成功したあと、ASP.NET Core のプロジェクトをデバッグ実行し、index.html にアクセスすると、GraphiQL を試せます。

f:id:miso_soup3:20171130143146p:plain

アクセスした時に HTTP をキャプチャすると、GraphiQL によって /graphql にスキーマを問い合わせるリクエストが送信されていることを確認できます。

参照