Xamarin.Forms+AppSync+Lambdaでデータを取得してみる

# 背景

昨年、Xamarin.FormsでKudanARアプリをリリースしました。
[Xamarin.FormsでKudan ARを試してみる](https://qiita.com/takapi_cs/items/581654f38ddb06a8e81c)

それから約1年が経過し、そろそろKudanARライブラリのAPIキーの使用期限が近づいています。
APIキーを切り替えるためだけに、都度アプリのリリースが必要になるのは面倒…
そこで、KudanARライブラリのAPIキーのデータだけLambdaで管理するよう切り離し、AppSyncを経由して取得することにしました。


# 環境

|主に使用したライブラリ|バージョン|
|---|---|
|Xamarin.Forms|v5.0.0.2012|
|GraphQL.Client|v3.2.4|
|GraphQL.Client.Serializer.Newtonsoft|v3.2.4|

|主に使用したAWS機能|
|---|
|AppSync|
|Lambda|


# 作成したもの

Lambdaからデータを返却し、AppSyncを経由してレスポンスを取得する点に焦点を当てたサンプルリポジトリを作成しました。
AWS側(AppSync、Lambda)に関しては`README.md`に記載しています。
当記事では省略している箇所が多々あるので、詳細はこちらをご覧ください。
[AppSyncDemo](https://github.com/takapi-Jr/AppSyncDemo)


# ソースコード

このサンプルでは、AppSyncに定義されている変数のデータをすべて取得するGraphQLにて実行すると、以下のようなレスポンスが返却されます。
Lambda自体は、以下の`GetSample`キーの値にあたる箇所を返却しています。

```json:name="Xamarin"で実行した場合のAppSyncレスポンス
{
  "data": {
    "GetSample": {
      "result": {
        "status_code": 200
      },
      "data": {
        "message": "Hello Xamarin!!!",
        "hoge": "abc",
        "fuga": "あいう",
        "piyo": "xyz",
        "foo": 123,
        "bar": 456
      }
    }
  }
}
```

Lambdaの返却値の中で、必要なデータが`message`のみだった場合、以下のようにGraphQLを定義します。
GraphQLでは、行の先頭に`#`をつけると以降がコメント行になるので、コメント行を使用して不要な変数を除いても構いません。

```graphql:フロントエンドで必要なデータを定義
query MyQuery($name: String) {
  GetSample(name: $name) {
    data {
      message
    }
  }
}
```

Modelクラスは、GraphQLに合わせて実装します。
今回はGraphQLの`data`の内側部分のみを対象としたいと思います。

```C#:SampleModel.cs
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text;

namespace AppSyncDemo.Models
{
    [DataContract]
    public class SampleModel
    {
        [DataMember(Name = "message")]
        public string Message { get; set; }
    }
}
```

以下のソースは、GraphQLを読み込んで、Queryとして実行する処理を行います。
GraphQLは、`AppSyncDemo`プロジェクトの`GraphQLs`フォルダに、**埋め込みリソース**として格納しています。

```C#:AppSyncService.cs
using AppSyncDemo.Models;
using GraphQL;
using GraphQL.Client.Http;
using GraphQL.Client.Serializer.Newtonsoft;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace AppSyncDemo.Services
{
    public class AppSyncService
    {
        public static AppSyncService Instance { get; private set; } = new AppSyncService();
        private GraphQLHttpClient GraphQLHttpClient { get; set; }

        public AppSyncService()
        {
            var options = new GraphQLHttpClientOptions
            {
                EndPoint = new Uri(ApiKey.AppSyncApiUrl),
            };
            this.GraphQLHttpClient = new GraphQLHttpClient(options, new NewtonsoftJsonSerializer());
            this.GraphQLHttpClient.HttpClient.DefaultRequestHeaders.Add("x-api-key", ApiKey.AppSyncApiKey);
        }

        public async Task<SampleModel> GetSampleAsync(string name)
        {
            var apiName = "GetSample";
            var variables = new 
            {
                name = name,
            };
            var response = await ExecQueryAsync<SampleModel>(apiName, variables);
            return response;
        }

        /// <summary>
        /// Queryを実行する
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="apiName"></param>
        /// <param name="variables"></param>
        /// <returns></returns>
        public async Task<T> ExecQueryAsync<T>(string apiName, object variables)
        {
            try
            {
                // GraphQL取得
                var resourceId = $"AppSyncDemo.GraphQLs.{apiName}.gql";
                var query = await GetQueryAsync(resourceId);

                // リクエスト作成
                var request = new GraphQLRequest
                {
                    Query = query,
                    OperationName = "MyQuery",
                    Variables = variables,
                };

                // Query実行
                // Mutation実行の場合はSendMutationAsync()を使用
                var response = await this.GraphQLHttpClient.SendQueryAsync<JObject>(request);
                // [apiName]から先が必要かどうかはレスポンスの構造に応じて変更する必要あり
                var json = response.Data[apiName]["data"].ToString();
                var ret = JsonConvert.DeserializeObject<T>(json);

                return ret;
            }
            catch (Exception)
            {
                return default(T);
            }
        }

        /// <summary>
        /// GraphQLを取得
        /// </summary>
        /// <param name="resourceId"></param>
        /// <returns></returns>
        private async Task<string> GetQueryAsync(string resourceId)
        {
            var assembly = Assembly.GetExecutingAssembly();
            using (var stream = assembly.GetManifestResourceStream(resourceId))
            using (var reader = new StreamReader(stream))
            {
                return await reader.ReadToEndAsync();
            }
        }
    }
}
```

一通りの流れは以上です。


元々の目的は、APIキーのデータのみ取得だったので、AppSync経由ではなくAPI Gateway経由でもよかったのですが、あまり記事を見かけなかったので、AppSyncの記事を作成しました。
AppSyncの記事をあまり見かけなかった、というよりも、C#とAppSyncを使用した記事をあまり見かけなかった、と言った方が正確かもしれません。
そもそも、「APIキーを取得するAPI」というのがあまりよろしくなさそうな気はしていますが、その点についてはあまり考えないことにしました。


# 参考リンク

[sagulati/dotnet-lambda-refarch-imagerecognition](https://github.com/sagulati/dotnet-lambda-refarch-imagerecognition/tree/master/image-recognition-infrastructure/src/s3Trigger/src/s3Trigger)
[C#で不定形JSONを自在に扱いたい](https://qiita.com/rana_kualu/items/b4fab77d4c5c4f6fd3da)