次の方法で共有



March 2018

Volume 33 Number 3

データ ポイント - ユニバーサル Windows プラットフォームから Azure Functions を呼び出す

Julie Lerman

EF Core 2 と UWP アプリ データに関する連載のすべてのコラムを読む:

EF Core 2 と Azure Functions を使用して UWP アプリ データをローカルとクラウドに保存する、パート 1
EF Core 2 と Azure Functions を使用して UWP アプリ データをローカルとクラウドに保存する、パート 2
EF Core 2 と Azure Functions を使用して UWP アプリ データをローカルとクラウドに保存する、パート 3
EF Core 2 と Azure Functions を使用して UWP アプリ データをローカルとクラウドに保存する、パート 4

Julie Lerman今回は、データをローカルとクラウドに保存するユニバーサル Windows プラットフォーム (UWP) アプリの作成方法を説明する連載の最終回です。連載第 1 回では、Cookie­Binge という名前の UWP ゲーム アプリを作成しました。このアプリでは、Entity Framework Core 2 (EF Core 2) を使用して、ゲームをプレイしたデバイスにスコアを保存しました。連載の第 2 ~ 3 回では、Azure Functions の関数をクラウドで作成して、ゲーム スコアを Microsoft Azure Cosmos DB データベースに保存し、そこから取得する方法を説明しました。この連載の最終回となる今回は、UWP アプリから先述の関数に要求を行う方法を確認します。要求では、ゲーム スコアの送信と、トップ スコアの取得および表示を行います。取得/表示するのは、自分が使ったすべてのデバイスのトップ スコアに加えて、世界中のプレイヤーのトップ スコアです。

ここで説明する方法では、プレイヤーがクラウドに登録できるようにし、さらに、使用している任意のデバイスをその登録情報に関連付けられるようにもします。この登録情報は、関数へのスコアの送信や関数からのスコアの取得に利用しますが、アプリでの登録処理部分については説明しません。とはいえ、登録処理用に作成した新しい関数のコードを含め、この処理に関連するコードはサンプル コードで確認できます。

これまでのおさらい

説明をスムーズに進めるために、これから統合しようとしている UWP アプリと関数について、簡単におさらいしましょう。

CookieBinge ゲームでは、プレイヤーがプレイを終えたときに、プレイ終了をアプリに知らせるための 2 つのボタンが表示されます。1 つは [Worth It] (価値あり) ボタンで、プレイが終了し、クッキーの暴食に満足したことを示します。もう 1 つは [Not Worth It] (価値なし) ボタンです。これらのクリック イベントへの応答として、ロジックにより BingeServices.RecordBinge メソッドが実行されます。このメソッドでは EF Core 2 を使用して、データをローカル データベースに保存します。

このアプリには、ローカル データベースから上位 5 件のスコアを表示する機能もあります。

これまでの何回かのコラムで、Azure Functions で 3 つの関数を作成しました。1 つ目の関数は、ゲーム スコアとプレイヤーの UserId (登録機能によって割り当てられたもの)、およびゲームをプレイしたデバイス名を取得し、その後、このデータを Cosmos DB データベースに保存します。残り 2 つの関数は、Cosmos DB データベースのデータに対する要求に応答するもので、1 つは、プレイヤーの UserId を受け取って、そのプレイヤーがプレイしたすべてのデバイスで記録されたすべてのスコアのうち、上位 5 件のスコアを返します。もう 1 つは、プレイヤーにかかわらず、データベースに保存されているすべてのスコアから上位 5 件のスコアを単純に返します。

それでは、これらの関数をゲームに統合する作業を始めましょう。まず、ゲーム スコアをローカル データベースに保存するタイミングで、アプリでは、スコアとその他の関連データを StoreScore 関数に送信する必要もあります。次に、アプリがローカル データベースからスコア履歴を読み取るタイミングで、アプリでは、スコアを返すように関数に要求を送信し、図 1 に示すように要求の結果を表示する必要もあります。

関数から取得した Azure Cosmos DB データベースのスコア
図 1 関数から取得した Azure Cosmos DB データベースのスコア

UWP からの Web との通信

UWP フレームワークでは、Web 要求の作成と応答の取得に特別な一連の API を使用します。実は、名前空間には 2 つの選択肢があるため、MSDN のブログ記事「ユニバーサル Windows プラットフォームにおける HttpClient API のわかりやすい解説」(bit.ly/2rxZu3f、英語) を参考にすることをお勧めします。ここでは、Windows.Web.Http 名前空間の API を使用します。この API には、要求と共にデータを送信する方法に関して、非常に具体的な要件があります。そのため、少し多くの手間がかかります。要求と共に JSON を送信する必要がある場合、HttpJsonContent というヘルパー クラスを利用して、JSON コンテンツとヘッダー コンテンツを組み合わせて、一定の追加ロジックを実行します。HttpJsonContent では、Windows.Data.Json.JsonValue の形式で JSON を送信する必要があります。後で、どこでこの手順を行うのかをお見せします。または、bit.ly/2BBjFNE で説明しているように、明示的に HttpRequest にヘッダー コンテンツを設定して、StringContent メソッドを使用して文字列として投稿できます。

関数とやり取りするロジックはすべて CloudService.cs という名前のクラスにカプセル化しました。

UWP 要求メソッドを使用するパターンと JsonValue オブジェクトの作成方法を特定してから、このロジックの大半をカプセル化する CallCookieBingeFunctionAsync という名前のメソッドを作成しました。このメソッドは、呼び出す関数の名前と JsonValue オブジェクトという 2 つのパラメーターを受け取ります。また、JsonValue オブジェクト パラメーターを必要としない、このメソッドのオーバーロードも作成しました。

そのメソッドのシグネチャは以下のとおりです。

private async Task<T> CallCookieBingeFunctionAsync<T>(string apiMethod,
  JsonValue jsonValue)

呼び出す必要のある関数は 3 つあるので、最も単純な GetTop5GlobalUserScores から始めましょう。この関数は、パラメーターやコンテンツなどを受け取らず、結果を JSON として返します。

CloudService クラスの GetTopGlobalScores メソッドは、新しい CallCookieBingeFunctionAsync メソッドを呼び出し、関数の名前を渡します。その後、その関数の応答を含む結果を返します。

public async Task<List<ScoreViewModel>> GetTopGlobalScores()
{
  var results= await CallCookieBingeFunctionAsync<List<ScoreViewModel>>
    ("GetTop5GlobalUserScores");
  return results;
}

このメソッドに、2 つ目のパラメーターを渡していないことに注目してください。つまり、JsonValue を必要としないオーバーロードが呼び出されます。

private async Task<T> CallCookieBingeFunctionAsync<T>(string apiMethod)
{
  return await CallCookieBingeFunctionAsync<T>(apiMethod, null);
}

これにより、このメソッドの別バージョンが呼び出され、JsonValue を想定しているパラメーターには単純に null が渡されます。以下に、CallCookieBingeFunctionAsync メソッドの全体を示します (このメソッドについては、確実に説明が必要でしょう)。

private async Task<T> CallCookieBingeFunctionAsync<T>(string apiMethod, JsonValue jsonValue)
{
  var httpClient = new HttpClient();
  var uri = new Uri("https://cookiebinge.azurewebsites.net/api/" + apiMethod);
  var httpContent = jsonValue != null ? new HttpJsonContent(jsonValue): null;
  var cts = new CancellationTokenSource();
  HttpResponseMessage response = await httpClient.PostAsync(uri,
    httpContent).AsTask(cts.Token);  string body = await response.Content.ReadAsStringAsync();
  T deserializedBody = JsonConvert.DeserializeObject<T>(body);
  return deserializedBody;
}

このメソッドでは、最初に、Windows.Web.Http.HttpClient のインスタンスを作成します。その後、HttpClient からの要求を作成するために、呼び出し先関数の Web アドレスをはじめとする必要な情報を構築します。今回作成した関数はすべて https://cookiebinge.azurewebsites.net/­api/ で始まります。そのため、この値をメソッドにハードコーディングして、その後、メソッドに渡す関数名を追加します。

次に、ヘッダーと、一緒に関数に渡すコンテンツを定義する必要があります。既に説明したように、この手順では、HttpJsonContent というヘルパー クラスを使用します。このクラスは、公式の Windows ユニバーサル サンプル (bit.ly/2ry7mBP、英語) の JSON セクションからコピーしました。このクラスにより、IHttpContent を実装するオブジェクトに JsonValue オブジェクトを変換できます (コピーしたクラスの全体は、サンプル コードで確認できます)。 GetTop5GlobalUserScores 関数のように、このメソッドに渡す JsonValue がない場合は、httpContent 変数は null になります。

このメソッドの次の手順は、変数 cts で CancellationTokenSource を定義することです。このコードではキャンセルを処理しませんが、読者の皆さんにこのパターンを把握してほしかったため、何はともあれ、トークンを含めています。

URI、httpContent、CancellationTokenSource といったすべての要素の構築が完了したので、ついに HttpClient.PostAsync メソッドを使用して関数を呼び出します。応答は JSON として返されます。コードでは、これを読み取り、JSON.Net の JsonConvert メソッドを使用して、呼び出し元メソッドで指定されたオブジェクトに応答をシリアル化解除します。

GetTopGlobalScores のコードを振り返ると、結果が List<ScoreViewModel> になるように指定していたことがわかります。ScoreViewModel は、2 つの関数から返されるスコア データのスキーマに一致するように作成した型です。このクラスには、UWP アプリでの表示方法に合わせてデータを書式設定する追加のプロパティもあります。Score­ViewModel クラスが長いリストだと考えて、このコードはダウンロード サンプルで調べられるようにする予定です。

パラメーターを受け取る関数の呼び出し

説明すべき関数がまだ 2 つあります。それでは、スコア データを返す方の関数を見ていきましょう。先述の関数は何も入力を受け取らなかったのに対して、この関数には UserId を渡す必要があります。しかし、今回の場合、HttpJsonContent を作成する必要はまだありません。というのも、先月説明したように、この関数は UserId の値を URI の一部として受け取るためです。先月取り上げた単純な例では、https://cookiebinge.azurewebsites.net/­api/GetUserScores/54321 という URI の文字列 54321 を UserId として使用しました。ID 管理機能をアプリに加えることで、UserId は GUID になります。

ユーザー ID の管理方法についてコードを詳細には説明しませんが、簡単に紹介しましょう。このコード全体は、ダウンロードで確認できます。ユーザー管理のために、新しい関数のペアを作成しました。プレイヤーがスコア追跡のためにクラウドに登録することを選ぶと、一方の関数によって、UserId として新しい GUID が作成され、その GUID が CookieBinge Cosmos DB データベースの個別のコレクションに登録され、UWP アプリに GUID が返されます。すると、UWP アプリでは、EF Core 2 を使用して、ローカル データベース内の新しいテーブルにその UserId を格納します。その GUID の一部は、図 1 に示すように、アカウント ページでプレイヤーに対して表示されます。プレイヤーが別のデバイスで CookieBinge ゲームをプレイした場合、他の登録済みのデバイスで確認できる部分的な GUID をもう一方の関数に送信することで、完全な GUID を取得できます。この関数は完全な GUID を返します。すると、アプリでは、その UserId を現在のデバイスに保存します。このように、プレイヤーは常に同じ UserId を使用して任意のデバイスからクラウドにスコアを送信します。さらに、アプリでは、同じ UserId を使用して、クラウドからそのプレイヤーがプレイしたすべてのデバイスのスコアを取得できます。AccountService.cs クラスには、ローカル データベースに UserId を保存し、ローカル データベースから UserId を取得する、UserId 関連のローカル操作用の機能があります。既存のフレームワークを活用することもできたでしょうが、このパターンを自力で思いついたときには、あまりにすばらしい発想に自分で自分を褒めました。

GetUserTopScores は、CloudServices に含まれるメソッドであり、GetUserScores 関数を呼び出します。先述のメソッドのように、CallCookieBingeFunctionAsync メソッドを呼び出します。今回も、戻り値の型は、ScoreViewModel オブジェクトのリストになります。再びパラメーターを 1 つだけ渡します。これは、関数の名前だけではなく、ベース URL に追加する完全な文字列です。文字列補間を使用して、関数名と AccountService.AccountId プロパティの結果を結合します。

public async Task<List<ScoreViewModel>> GetUserTopScores()
{
  var results = await CallCookie­Binge­Function­Async<List<ScoreViewModel>>
    ($"GetUserScores\\­{AccountService.AccountId}");
  return results;
}

要求に JSON コンテンツが含まれることを想定する関数の呼び出し

最後の関数は StoreScores です。これは、JSON を HttpRequest に追加する方法を説明するのにぴったりです。StoreScores は、JSON オブジェクトを受け取り、そのデータを Cosmos DB データベースに格納します。図 2 は、所定のスキーマに従う JSON オブジェクトを送信して、この関数をテストしたときの Azure Portal の表示を示しています。

JSON 要求本文を使用してテストした StoreScores 関数の Azure Portal ビュー
図 2 JSON 要求本文を使用してテストした StoreScores 関数の Azure Portal ビュー

UWP アプリのこのスキーマに合わせるために、StoreScoreDto という名前のデータ転送オブジェクト (DTO) 構造体を作成しました。これは、要求の JSON 本文を作成するのに役立ちます。以下に CloudService.SendBingeToCloudAsync メソッドを示します。このメソッドは、ゲームをプレイした結果のデータを受け取り、他の 2 つの関数の呼び出しに使用したのと同じ CallCookieBingeFunctionAsync メソッドを利用してデータを StoreScores 関数に送信します。

public async void SendBingeToCloudAsync(int count, bool worthIt,
  DateTime timeOccurred)
{
  var storeScore = new StoreScoreDto(AccountService.AccountId,
                                     "Julie", AccountService.DeviceName,
                                     timeOccurred, count, worthIt);
  var jsonScore = JsonConvert.SerializeObject(storeScore);
  var jsonValueScore = JsonValue.Parse(jsonScore);
  var results = await CallCookieBingeFunctionAsync<string>("StoreScores",
    jsonValueScore);
}

SendBingeToCloudAsync はまず、保存するプレイに関する関連データ (食べたクッキーの数、[Worth It] と [Not Worth It] のどちらをクリックしたか、およびプレイ時刻) を受け取ります。その後、そのデータから StoreScoreDto オブジェクトを作成し、再度 JsonConvert を使用します。今回は、StoreScoreDto を JSON オブジェクトにシリアル化します。次の手順では、JsonValue を作成します。これは、既に説明したように、Windows.Json.Data 名前空間の特別な型です。これを、JsonValue.Parse メソッドを使用し、jsonScore で表わされる JSON オブジェクトを渡して行います。これで作成される JsonValue は、JSON オブジェクトを HTTP 要求と一緒に送信するのに必要な形式です。JsonValue の形式を適切に設定できたので、これを StoreScores という関数名と一緒に CallCookeBingeFunctionAsync に送信できます。想定する戻り値の型は文字列です。これは、関数の成功または失敗を表わす StoreScores 関数からの通知になります。

UI と CloudService との結び付け

CloudService のメソッドを準備したら、最後に、UI がそのメソッドとやり取りするようにします。プレイを保存するときには、MainPage.xaml.cs のコードによって、プレイ データをローカル データベースに保存する BingeService のメソッドが呼び出されます。図 3 に示しているのはこれと同じメソッドですが、プレイ データを CloudService に送信して StoreScores 関数を通じてクラウドにも保存するように変更しています。

図 3 プレイ データをクラウドに送信するように変更した RecordBinge メソッド

public static  void RecordBinge(int count, bool worthIt)
{
  var binge = new CookieBinge{HowMany = count, WorthIt = worthIt,
                              TimeOccurred = DateTime.Now};
  using (var context = new BingeContext(options))
  {
    context.Binges.Add(binge);
    context.SaveChanges();
  }
  using (var cloudService = new BingeCloudService())
  {
    cloudService.SendBingeToCloudAsync(count, worthIt, binge.TimeOccurred);
  }
}

この関数とやり取りする他のメソッドはどちらも、ScoreViewModel オブジェクトのリストを返します。

クラウドに保存されたスコアを図 1 のように表示するために、CloudService の各種メソッドを呼び出してスコアを取得し、そのスコアをページ上の関連する ListViews にバインドするメソッドをMainWindow.xaml.cs に追加しました。このメソッドは、同じページの更新ボタンを使用した場合も呼び出されるので、ReloadScores という名前にしました。

private async Task ReloadScores()
{
  using (var cloudService = new BingeCloudService())
  {
    YourScoresList.ItemsSource = await cloudService.GetUserTopScores();
    GlobalScoresList.ItemsSource =
      await cloudService.GetTopGlobalScores();
  }
}

その後、UI はページの各リストに対して定義されたテンプレートに基づいてスコア データを表示します。たとえば、図 4 は、UI に GlobalScores を表示するための XAML を示しています。

図 4 関数から返されるスコア データをバインドする XAML データ

<ListView  x:Name="GlobalScoresList"    >
  <ListView.ItemTemplate>
    <DataTemplate >
      <StackPanel Orientation="Horizontal">
        <TextBlock FontSize="24" Text="{Binding score}"
                   VerticalAlignment="Center"/>
        <TextBlock FontSize="16" Text="{Binding displayGlobalScore}"
                   VerticalAlignment="Center" />
      </StackPanel>
    </DataTemplate>
  </ListView.ItemTemplate></ListView>

この 4 部構成連載のまとめ

この連載は最新バージョンの EF Core 2 を Windows ベースのモバイル デバイスで使う試みとして始めたものでしたが、わくわくするような探究ができました。読者の皆さんにも、楽しく、刺激の多い、有意義な連載だと感じていただけたら光栄です。新しい .NET Standard 2.0 ベースの UWP に (特に、プレリリースの早い段階で) 取り組んだことは、バックエンド開発者である私にとって難しい挑戦だったことは間違いありません。しかし、ローカルとクラウドの両方にデータを保存でき、取り組みながら新しいスキルを習得するという考えは非常に魅力的でした。

連載の第 2 回と第 3 回で、初めて Azure Functions を使いました。ここで Azure Functions を利用する口実ができて、とてもよかったと思っています。今では Azure Functions の大ファンになり、さまざまなことに Azure Functions を使用しています。皆さんも同じようにインスピレーションを感じられましたらさいわいです。

本稿を読んでおわかりのように、UWP アプリから関数とやり取りするのは、過去に他のプラットフォームから Web 呼び出しを行った経験と比べると、それほど簡単ではありません。このワークフローを理解して、個人的に大きな満足感を得ました。

コード ダウンロードをチェックすると、今回このアプリに追加した、クラウド ストレージへの登録、Azure で生成した UserId やデバイス名の保存、追加デバイスの登録、StoreScores および Get­UserScores のメソッドで使用する UserId とデバイス名へのアクセスなどのロジックすべてを確認できます。アプリをサポートするすべての関数を確認して操作できるように、Azure Function App 全体を .NET プロジェクトにダウンロードしてあります。ID ワークフローを明らかにするのに驚くほどの時間を使いました。この取り組みの楽しさに少し夢中になりました。おそらく、この経験についてもいつかコラムにすることがあるでしょう。


Julie Lerman は、バーモント ヒルズ在住の Microsoft Regional Director、Microsoft MVP、ソフトウェア チームの指導者、およびコンサルタントです。世界中のユーザー グループやカンファレンスで、データ アクセスなどのトピックについてプレゼンテーションを行っています。彼女のブログは thedatafarm.com/blog (英語) で、彼女は O'Reilly Media から出版されている『Programming Entity Framework』(2010 年) および『Code First』版 (2011 年)、『DbContext』版 (2012 年) を執筆しています。彼女の Twitter (@julielerman、英語) をフォローして、juliel.me/PS-Videos (英語) で彼女の Pluralsight コースをご覧ください。

この記事のレビューに協力してくれた技術スタッフの Ginny Caughey (Carolina Software Inc.) に心より感謝いたします。
Ginny Caughey は、米国とカナダの固形廃棄物産業にソフトウェアとサービスを提供している Carolina Software, Inc. の代表取締役です。余暇を使って、Windows および Windows Phone 向けの Password Padlock アプリを作成したこともあります。Twitter (@gcaughey、英語) で活発に発言しており、Windows Development MVP でもあります。


この記事について MSDN マガジン フォーラムで議論する