miso_soup3 Blog

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

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 にスキーマを問い合わせるリクエストが送信されていることを確認できます。

参照

作業ログ: Upgrade Umbraco from 7.5.x to 7.7.x

Umbraco を 7.5.x から 7.7.x へ更新したので、作業ログを残しておきます。
v7.6 は 2017/5に、v7.7 は2017/9にリリースされました。

Umbraco の更新では、次の2つのドキュメントを参照します。

大まかに機能を知るためには、ブログを参照しました:v7.6v7.7

今回の更新では、v7.6.0 へのアップグレードにていつもと違う対処が必要でした。先の Version specific upgrades のドキュメントにて、3つの注意点と NuGet での更新についての項目があります。やるべきことは記載されていますが、自分のケースでの対応を書いておきます。

  • 1 の useLegacyEncoding
    • Web.config にて、useLegacyEncoding の設定が true だったので、そのままにしました。
  • 2 の In umbracoSettings.config
    • EnablePropertyValueConverters の設定が元々無かったので、何も対応しませんでした。
  • 3 の tinyMceConfig.config
    • NuGet で更新した際に、<plugin loadOnFrontend="true">umbracolink</plugin> が削除されたので、そのままとしました。
NuGet での更新について

v7.6 にて、Umbraco の URL Rewrite の機能(config/UrlRewriting.config で記述する方法) が無くなった模様です。つまり、UrlRewriting.Net パッケージへの依存が削除されました(参照:
Umbraco 7.6 Breaking Changes
)。UmbracoCms パッケージを NuGet で更新した際に、config/UrlRewriting.config ファイルが削除されましたが、Web.config からの参照は削除されませんでした。アップデート後、この削除の手順を行わずに実行してみると、次のようにエラー画面が表示されました。

f:id:miso_soup3:20171127143224p:plain

対処はドキュメントにもあるとおり、次のように行いました。すべて Web.config の編集です。

  • <section name="urlrewritingnet" restartOnExternalChanges="true" requirePermission="false" type="UrlRewritingNet.Configuration.UrlRewriteSection, UrlRewritingNet.UrlRewriter" /> を削除。
  • <urlrewritingnet configSource="config\UrlRewriting.config" /> を削除
  • httpModules要素内の、<add name="UrlRewriteModule" ... の要素を削除。system.webServer要素内の、<remove name="UrlRewriteModule"...と、<add name="UrlRewriteModule"...の要素を削除

自分のケースでは、Umbraco の URL Rewrite ではなく IIS の URL Rewrite を使っていましたが、この対応をした後も問題なく動作しました。