Computer Vision APIでランドマーク認識する

本日は天気が良く、最近にしては珍しく予定が何もなかったのでプチドライブにいってきました。

我が家のある夢の国を出発し、永代橋を渡り、銀座を通り抜け、東京タワーへ、そしてレインボーブリッジ(下の一般道)を渡って夢の国に舞い戻る・・・という。

で、日本が誇るランドマーク「東京タワー」をカメラで撮ってきました。

ランドマークといえば先日2017/4/19に Cognitive Services のいくつかに機能が GA していました・・・(Face API / Computer Vision API / Content Moderator)

ということで、Computer Vision APIのランドマーク認識は、私の撮った写真をきちんと認識してくれるのかどうか試してみましょう!

Computer Vision APIよ、これが何か分かるか!?

f:id:daigo-knowlbo:20170423163959p:plain
※今日は、なんか、真ん中の展望台辺りから霧のようなものが噴霧されていました。

1. Azureに「Cognitive Services APIs」を作成

Cognitive Servicesを利用するためにはAzureに「Cognitive Services APIs」を作成する必要があります。
Azureポータルにログインします。
「+新規」をクリック。

f:id:daigo-knowlbo:20170423161222p:plain

Cognitive Services APIsを選択。

f:id:daigo-knowlbo:20170423161258p:plain

f:id:daigo-knowlbo:20170423161308p:plain

作成。

f:id:daigo-knowlbo:20170423161328p:plain

各種パラメータを適時設定します。

f:id:daigo-knowlbo:20170423161419p:plain
ここでは以下の設定にしました。

  • アカウント名: MyCognitive1
  • APIの種類: 今回はランドマーク認識を行うので「Computer Vision API」となります。
  • 場所: 日本のリージョンはまだないので「東南アジア」にしました。
  • 価格レベル: テストなので無料の F0 にしました(F0は、1分あたり20回・1ケ月あたり5000回の呼び出し制限)。

しばらく待つと作成が完了します。
以下はリソースの一覧画面で確認したところです。

f:id:daigo-knowlbo:20170423161851p:plain

MyCognitive1を選択し「概要」画面で「エンドポイント」をコピーしておきます。

f:id:daigo-knowlbo:20170423161943p:plain

「キー」を選択し「キー1」をコピーしておきます。

f:id:daigo-knowlbo:20170423162047p:plain

エンドポイントとキーはプログラムからAPI呼び出しを行う際に利用します。

2. コンソールアプリを作成

Visual Studio 2017(2015でもいいけど)を起動し、コンソールアプリケーションを作成します。
(前述で作成したCognitive Services(Computer Vision API)を呼び出すアプリです)

で、実装は単純に以下のサンプルをそのまま拝借させていただきました。

azure.microsoft.com

// https://azure.microsoft.com/ja-jp/blog/microsoft-cognitive-services-general-availability-for-face-api-computer-vision-api-and-content-moderator/から引用
// 名前空間、キー設定部分、エンドポイント指定部分を修正しています。

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;

namespace RecognizinglandmarksExample
{
  static class Program
  {
    static void Main()
    {
      Console.Write("Enter image file path: ");
      string imageFilePath = Console.ReadLine();

      MakeAnalysisRequest(imageFilePath);

      Console.WriteLine("\n\nHit ENTER to exit...\n");
      Console.ReadLine();
    }

    static byte[] GetImageAsByteArray(string imageFilePath)
    {
      FileStream fileStream = new FileStream(imageFilePath, FileMode.Open, FileAccess.Read);
      BinaryReader binaryReader = new BinaryReader(fileStream);
      return binaryReader.ReadBytes((int)fileStream.Length);
    }

    static async void MakeAnalysisRequest(string imageFilePath)
    {
      var client = new HttpClient();

      // Request headers. Replace the second parameter with a valid subscription key.
      //client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", "putyourkeyhere");
      client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", "あなたのキーを設定");

      // Request parameters. You can change "landmarks" to "celebrities" on requestParameters and uri to use the Celebrities model.
      string requestParameters = "model=landmarks";
      //string uri = "https://westus.api.cognitive.microsoft.com/vision/v1.0/models/landmarks/analyze?" + requestParameters;
      string uri = "https://southeastasia.api.cognitive.microsoft.com/vision/v1.0/models/landmarks/analyze?" + requestParameters;
      
      Console.WriteLine(uri);

      HttpResponseMessage response;

      // Request body. Try this sample with a locally stored JPEG image.
      byte[] byteData = GetImageAsByteArray(imageFilePath);

      using (var content = new ByteArrayContent(byteData))
      {
        // This example uses content type "application/octet-stream".
        // The other content types you can use are "application/json" and "multipart/form-data".
        content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
        response = await client.PostAsync(uri, content);
        string contentString = await response.Content.ReadAsStringAsync();
        Console.WriteLine("Response:\n");
        Console.WriteLine(contentString);
      }
    }
  }
}

上記ソースから以下の行を修正します。

client.DefaultRequestHeaders.Add(“Ocp-Apim-Subscription-Key”, “あなたのキーを設定”);

「あなたのキーを設定」部分には、Azureポータルからの「Cognitive Services APIs」作成で得られた「キー1」に置き換えます。

もう1ヶ所、以下のエンドポイント部分も修正対象ですが、こちらは上記ソースでは東南アジア用に修正してあります。

string uri = “https://westus.api.cognitive.microsoft.com/vision/v1.0/models/landmarks/analyze?” + requestParameters;

ビルドしてコンソールアプリを作成します。
(このコンソールアプリでは、Vision APIの呼び出しに単純なHTTP POSTを行っているだけなので、特別なNugetパッケージの追加は必要ありません)

3. 実行

CTRL + F5で実行します。
画像ファイル名を聞かれるので、入力してEnter。

f:id:daigo-knowlbo:20170423163309p:plain

認識の結果がJSONで帰ってきます。

f:id:daigo-knowlbo:20170423163317p:plain

「Tokyo Tower」正解!そして confidence は 0.9743892 とのこと。

まとめ

本投稿は Cognitive Services の本当に触りの部分の薄っぺらな内容でしたが、顔認識・文字認識等々色々な機能が提供されています。BotとかMachine Learningとかと組み合わせるといろいろなことが出来そうですよね。

Global Azure Bootcamp 2017@Tokyo に行ってきた

本日(2017/4/22)、Global Azure Bootcamp 2017@Tokyoに参加してきました。

Global Azure Bootcamp 2017@Tokyo - connpass

ということで、ざっくり(長々?)個人的感想を垂れ流しておきます。

Azureって何よ2017年の最新情報をゆるまとめ

JAZUG女子部の安東 沙織さん・鈴木 可奈さんのセッション。

www.slideshare.net

Azureの概要、そして、この1年のAzureアップデートのまとめを、お二人の軽快な掛け合いトークで説明していただきました。
各技術に深入りするわけではなく、でも実際に日々Azureを使っている中でのポイント的なことであったり、不満点だったりについてお話していただきました。
あと、従来、Azure全体の説明を行う場合、「IaaSの説明をから入り、PaaSの説明に移る」ことが多いですが、「(お二人曰く)そろそろあえてPaaSを先に持ってきた」という点が「うん!いいね!」って思いました。
本スライドで、1年間をササーっと振り返り、箇条書き以上の内容は「ブチザッキ | いわゆる雑記」で、さらにここの技術を深掘る場合は公式Docや公式Blogへ、というところでしょうか。

Azure 2017年3月障害Deep Dive

harutamaさんのセッション。

docs.com

このセッション、事前に想像していたのと違っていて面白かったです。
2017年3月の障害に対するMSの報告文書「Root Cause Analysis」に記された「ストレージサービスのStream Manager」なるものの正体(実体)についてのDeepDiveでした。

ストレージサービスについては以下のようなことは、多くのAzure利用者が把握していること。

  • blobとかtableとかqueueとかfileとかの種類があるよね
  • LRSとかZRSとかGRSとかRA-GRSとかのレプリケーションオプションで耐障害性レベルが上がるよね
  • Premium Storage使うとVirtualMachine1台構成でも99.9%のSLAが付くよね

でも本セッションは、その先のストレージの内部構造についてのお話でした。 この内部構造は、Build 2016におけるセッションスライドとかMicrosoft Researchの論文とかをよく読むと紐解ける情報とのこと(確かそうだったよね・・・記憶あいまい気味)。
この手の話は実は大好きなので楽しかったです。
で、Azureは新機能連発なので最新ドキュメントにばかり目が行ってしまうのですが、過去の論文もしっかり読んでおくべきだと思いました。

bitFlyerを支える仕組み - Azure障害の中1時間未満でサービス復旧を実現-

竹井 悠人さん・萬藤 和久さんのセッション。

www.slideshare.net

ブロックチェーン・ビットコインのシステムを開発・運用しているbitFlyerさんのAzure障害との闘い(?)のご紹介でした。
時系列でAzure障害によるシステムの停止から別リージョンでの復旧の試み、3度の大規模障害から得られたノウハウ(障害対応中に問い合わせたマイクロソフトのサポートからも得られなかったノウハウ達)について説明していただきました。
ここら辺の話を聞きつつ、典型的エンプラの「ノウハウが蓄積されていない事には踏み入れない」精神だとAzureはないんだろうなぁ、とか思いました。私個人的には立ち向かい、それに見合うだけ得られるものを得ていくエンジニア人生を歩みたいです。

サポートエンジニアが紹介する、Azure Portal/CLI 使いこなし

田中孝桂さんのセッション。

docs.com

最近、個人的にも Azure CLI v2 をちょろちょろと使っていたので、でも、その時必要なコマンドのみで、Azure AutomationでVMの自動停止等々 細かなコマンドを知らなかったので、いろいろ勉強になりました。

Azure ML系 「practice over theory」

梅崎 猛さんのセッション。

docs.com

AzureML / Cognitive Service / Bot Framework with QnA / CNTK についてザザーと「こんなすごいのが、こんなに簡単に使えるよー」というビデオデモをたくさん流していただきました。
個人的には、Cognitive Serviceのいくつかは、数日前にGAしていたので、ちょうどお試し利用していたところでした。
AzureMLについては実は使ったことがなかったので、へー、こんな感じで使えるんだあ、と思いました。
Bot Framework with QnAなんかは、私が勤めている会社は自社サービスを主軸として製品サポート部門なんかもあるので、そこらへんにBotや問い合わせ文自動解析のCognitive Serviceがうまく使えればサポートコスト削減にならないだろうか、とぼんやり考えました(実際に実現するのはすんごい大変で、おそらく現段階では会社としては取り組まないだろうし、現実的ではないと思っています)。

DocumentDB DeepDive

近江 武一さんのセッション。

www.slideshare.net

私は最近、DocumentDBに はまって いて、公式ドキュメントを結構読み漁ったので、うんうんって思いながら聞くことができました。
また、以下のことは知らなかったので興味深かったです。

  • 「1KBデータだったら1RU消費」と思っていたのが、その物自体だけでないコストが発生して 2RU くらい消費する 。
  • RUはパーティションごとに等分されて割り当てられる。データが特定パーティションに偏ると、設定RUが無駄に死んだ状態になる。
  • パーティションが内部的には25個に分割されているみたい?(検証中?)

セッション内で、MSの松崎剛さんのブログが紹介されていましたが、このブログ、私は見逃していたのですが、内容的に非常に興味深い有益なものでした。

blogs.msdn.microsoft.com

松崎さんは実は弊社の ISVパートナー担当エバンジェリスト(?)(正式な立ち位置名称が分からないのですが・・・) をしていただいておりまして、いろいろ相談可能・・・なのですが・・・ここら辺のAzure周りのテクノロジーに対する取り組みは、私個人の活動であり、会社としてはあまりAzureには力を入れていないので、つまりアポできませんねぇ・・・

オルターブースさんのAzure事例

松村 優大さん、森田 邦裕さんのセッション。

docs.com

オリジナルソース(ドレッシング)をオーダーできるWebサイトを自社開発・運営されている株式会社オルターブースさんの事例紹介でした。
私は、このセッションを聞いてオルターブースさんのファンになりました。

技術に対して 挑戦的で熱い会社 であると感じたからです。

RCの段階で.NET Core採用しちゃったり、使ってみたい技術で行こうよ!って精神だったり(結果、今時の技術全部乗せ的な)、AzureとAWSの併用活用だったり、Slackへの障害通知、Fulentd→elastic search→kibanaな可視化ロギングだったり、Azure Container / Docker swarmで運用してたり、社員旅行と称してみんなで DE:CODE 2016 行くとか(半分冗談?半分ほんと?)。
私もソース、オーダーさせていただきます!

最後に

あー、あと、お声がけしたい方が何人かいたのですが、タイミングを逃し、そしてコミュ障パワーを発揮しお声がけできませんでした・・・
技術的に聞いてみたいこともあるので、また機会があったらお声がけさせていただきます!
この系統のイベントには今後も引き続き参加させていただきたいと思いますので、皆様よろしくお願いいたします。

DocumentDBのgeospatial機能で地理的検索(位置情報検索)を行う

結構以前からあったものだと思うのですが、Azure DocumentDBの「geospatial(地理空間検索)」機能を使ってみたいと思います。
緯度・経度情報をDocumentDBに保存しておき、以下のような操作を行うことが可能です。

  1. 2点間の距離の算出
  2. 1つの地理的領域が、別のもう1つの領域内に存在するかのチェック
  3. 2つの地理的領域の重なりのチェック

ここでは「地理的領域」という言葉を使用しましたが、DocumentDBでは緯度・経度情報を「点」「ライン」「ポリゴン(領域)」で保持することができます。
これらの緯度・経度情報に対してクエリーをかけることができます。

こんなアプリを作る

本投稿では、上記1番目の「2点間の距離の算出」を利用して以下のようなWebアプリを作成します。

  • 災害時避難施設情報をDocumentDBに保存しておく
  • Webアプリを作成。JavaScriptで現在位置情報を自動取得 → 近隣の避難施設を検索する
  • 避難施設の場所はGooogle Mapsへのリンクを配置することで表示する

画面キャプチャは以下の通りです。

f:id:daigo-knowlbo:20170418235759p:plain

「避難施設を検索」をクリックすると・・・

f:id:daigo-knowlbo:20170418235920p:plain

「地図を表示」をクリックすると・・・Google Mapsが別ウィンドウ表示される。

f:id:daigo-knowlbo:20170418235933p:plain

ソースは以下の github に上げてあります。

github.com

実装の構成は「1ソリューション・3プロジェクト構成」です。
プロジェクト構成は以下の通り。

・CreateEvacuationFacilityDb - DocumentDBデータを作成するコンソールアプリケーションプロジェクト
・EvacuationFacilityLib - DocumentDB操作を行う為のリポジトリクラスとモデルクラスを含むライブラリプロジェクト
・EvacuationFacilityWeb - 災害時避難施設を検索するWebアプリケーションプロジェクト

では以下に、ソースの解説を含めて作成手順を記述いたします。

1. DocumentDBデータを準備する

まず、データソースとなるDocumentDBデータを作成します。

DocumentDB Emulatorを起動

Azure上にDocumentDBを作成してもよいのですが、今回はローカルでお手軽にテスト可能な DocumentDB Emulator を利用します。
DocumentDB Emulatorを起動しておきましょう。タスクトレイにブルーのアイコンが現れますね。

災害時避難施設情報XMLを取得する

災害時避難施設情報は 国土数値情報ダウンロードサービス国土数値情報 避難施設データの詳細 からダウンロードすることができるのでこれを利用します。
平成24年のデータで古い気もしますが、全国の災害時避難施設の名称・住所・緯度経度情報 等々 が得られます。
今回は、データが大きいので、東京 のデータだけを取得しました。
ダウンロードファイルを解凍すると以下のようなイメージです。

f:id:daigo-knowlbo:20170418203917p:plain

この中から「P20-12_13.xml」を利用します。

避難施設情報XMLからDocumentDB保存用データ型に変換

XMLデータをDocumentoDBデータ保存用の型に変換する処理を用意します。
ダウンロードした P20-12_13.xml は以下のようなスキーマです。

// P20-12_13.xml
<?xml version="1.0" encoding="utf-8"?>
<ksj:Dataset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ksj="http://nlftp.mlit.go.jp/ksj/schemas/ksj-app" gml:id="P20Dataset" xsi:schemaLocation="http://nlftp.mlit.go.jp/ksj/schemas/ksj-app KsjAppSchema-P20-v1_0.xsd">
  <gml:description>国土数値情報 避難施設 インスタンス文書</gml:description>
  <gml:boundedBy>
    <gml:EnvelopeWithTimePeriod srsName="JGD2000 / (B, L)" frame="GC / JST">
      <gml:lowerCorner>20.0 123.0</gml:lowerCorner>
      <gml:upperCorner>46.0 154.0</gml:upperCorner>
      <gml:beginPosition calendarEraName="西暦">1900</gml:beginPosition>
      <gml:endPosition indeterminatePosition="unknown"/>
    </gml:EnvelopeWithTimePeriod>
  </gml:boundedBy>
  <!--図形-->
  <gml:Point gml:id="pt100001">
    <gml:pos>35.686898 139.739538</gml:pos>
  </gml:Point>
  <gml:Point gml:id="pt100002">
    <gml:pos>35.697347 139.760433</gml:pos>
  </gml:Point>
  ... 省略 ...
  <ksj:EvacuationFacilities gml:id="P20_100001">
    <ksj:position xlink:href="#pt100001"/>
    <ksj:administrativeAreaCode codeSpace="AdministrativeAreaCode.xml">13101</ksj:administrativeAreaCode>
    <ksj:name>いきいきプラザ一番町</ksj:name>
    <ksj:address>東京都千代田区一番町12-2</ksj:address>
    <ksj:facilityType>指定避難所、二次避難所</ksj:facilityType>
    <ksj:seatingCapacity>-1</ksj:seatingCapacity>
    <ksj:facilityScale>-1</ksj:facilityScale>
    <ksj:hazardClassification>
      <ksj:Classification>
        <ksj:earthquakeHazard>false</ksj:earthquakeHazard>
        <ksj:tsunamiHazard>false</ksj:tsunamiHazard>
        <ksj:windAndFloodDamage>false</ksj:windAndFloodDamage>
        <ksj:volcanicHazard>false</ksj:volcanicHazard>
        <ksj:other>false</ksj:other>
        <ksj:notSpecified>true</ksj:notSpecified>
      </ksj:Classification>
    </ksj:hazardClassification>
  </ksj:EvacuationFacilities>
  <ksj:EvacuationFacilities gml:id="P20_100002">
    <ksj:position xlink:href="#pt100002"/>
    <ksj:administrativeAreaCode codeSpace="AdministrativeAreaCode.xml">13101</ksj:administrativeAreaCode>
    <ksj:name>お茶の水小学校</ksj:name>
    <ksj:address>東京都千代田区猿楽町1-1-1</ksj:address>
    <ksj:facilityType>指定避難所、地区救援センター</ksj:facilityType>
    <ksj:seatingCapacity>-1</ksj:seatingCapacity>
    <ksj:facilityScale>-1</ksj:facilityScale>
    <ksj:hazardClassification>
      <ksj:Classification>
        <ksj:earthquakeHazard>false</ksj:earthquakeHazard>
        <ksj:tsunamiHazard>false</ksj:tsunamiHazard>
        <ksj:windAndFloodDamage>false</ksj:windAndFloodDamage>
        <ksj:volcanicHazard>false</ksj:volcanicHazard>
        <ksj:other>false</ksj:other>
        <ksj:notSpecified>true</ksj:notSpecified>
      </ksj:Classification>
    </ksj:hazardClassification>
  </ksj:EvacuationFacilities>
  ... 省略 ...
</ksj:Dataset>

上記XMLデータを元に、以下のようなJSONスキーマデータに変換してDocumentDBに保存したいと思います。

{
  "ID": "pt100001",
  "Latitude": 35.686898,
  "Longitude": 139.739538,
  "Location": {
    "type": "Point",
    "coordinates": [
      139.739538,
      35.686898
    ]
  },
  "Name": "いきいきプラザ一番町",
  "Address": "東京都千代田区一番町12-2",
  "FacilityType": "指定避難所、二次避難所"
}

ということで、上記JSONスキーマに該当するモデルクラスとして、以下の EvacuationFacilityInfo クラスを用意します。
「public Point Location」プロパティがポイントで、Point型は「Microsoft.Azure.Documents.Spatial名前空間の型」になります。
この値が地理的データクエリーの対象となります。

// EvacuationFacilityLib\Models\EvacuationFacilityInfo.cs
using Microsoft.Azure.Documents.Spatial;

namespace EvacuationFacilityLib.Models
{
  public class EvacuationFacilityInfo
  {
    // ID
    public string ID { get; set; }
    // 緯度
    public double Latitude { get; set; }
    // 経度
    public double Longitude { get; set; }
    // DocumentDBクエリー用のPoint型
    public Point Location { get; set; }
    // 施設名
    public string Name { get; set; }
    // 住所
    public string Address { get; set; }
    // 施設タイプ
    public string FacilityType { get; set; }
  }
}

XMLデータを EvacuationFacilityInfo オブジェクト配列にコンバートするロジックを実装します(GmlToCustomFormatConvertorクラス)。
実装はごちゃごちゃしていますが、「XPathでクエリーしてモデルクラスに一生懸命変換する」というごり押し実装なので説明は省きます。

// CreateEvacuationFacilityDb\GmlToCustomFormatConvertor.cs
using System.Collections.Generic;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Linq;
using Microsoft.Azure.Documents.Spatial;
using EvacuationFacilityLib.Models;

namespace CreateEvacuationFacilityDb
{
  public class GmlToCustomFormatConvertor
  {
    public static List<EvacuationFacilityInfo> Load(string xmlFilePath)
    {
      var result = new List<EvacuationFacilityInfo>();

      var xDoc = XDocument.Load(xmlFilePath);

      var nsmgr = new XmlNamespaceManager(new NameTable());
      nsmgr.AddNamespace("gml", "http://www.opengis.net/gml/3.2");
      nsmgr.AddNamespace("ksj", "http://nlftp.mlit.go.jp/ksj/schemas/ksj-app");
      nsmgr.AddNamespace("xlink", "http://www.w3.org/1999/xlink"); 

      var points = xDoc.XPathSelectElements("//gml:Point", nsmgr);
      foreach (var point in points)
      {
        var evacuationFacilityInfo = new EvacuationFacilityInfo();

        //var id = point.Attribute("gml:id").Value;
        evacuationFacilityInfo.ID = point.FirstAttribute.Value;
        
        //var pos = point.XPathSelectElement("gml:pos");
        string latitudeAndLongitude = point.Value;
        double dLatitude = double.Parse(latitudeAndLongitude.Split(' ')[0]);
        double dLongitude = double.Parse(latitudeAndLongitude.Split(' ')[1]);
        evacuationFacilityInfo.Latitude = dLatitude;
        evacuationFacilityInfo.Longitude = dLongitude;
        evacuationFacilityInfo.Location = new Point(dLongitude, dLatitude);

        var evacuationFacility = 
          xDoc.XPathSelectElement("//ksj:EvacuationFacilities/ksj:position[@xlink:href='#" + evacuationFacilityInfo.ID + "']", nsmgr).Parent;
        evacuationFacilityInfo.Name = 
          evacuationFacility.XPathSelectElement("ksj:name", nsmgr).Value;
        evacuationFacilityInfo.Address = 
          evacuationFacility.XPathSelectElement("ksj:address", nsmgr).Value;
        evacuationFacilityInfo.FacilityType = 
          evacuationFacility.XPathSelectElement("ksj:facilityType", nsmgr).Value;

        result.Add(evacuationFacilityInfo);
      }

      return result;
    }
  }
}

EvacuationFacilityInfoオブジェクトをDocumentDBに保存

XMLをコンバートして得られたEvacuationFacilityInfoオブジェクトをDocumentDBに保存するリポジトリークラスも作成しましょう。
データベースの作成・コレクションの作成・ドキュメントの保存・ドキュメントの地理的検索まで実装してしまいます。

// EvacuationFacilityInfoRepository.cs
#define LOCAL_DB

using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;

using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;

using Microsoft.Azure.Documents.Spatial;

using EvacuationFacilityLib.Models;

namespace EvacuationFacilityLib.Repositories
{
  public class EvacuationFacilityInfoRepository
  {
    /// <summary>
    /// 任意のデータベースID
    /// </summary>
    private static readonly string DatabaseId = "EvacuationFacilityInfoDb";

    /// <summary>
    /// 任意のコレクションID
    /// </summary>
    private static readonly string CollectionId = "EvacuationFacilityInfoCollection";

    /// <summary>
    /// エンドポイント
    /// </summary>
#if LOCAL_DB
    private static readonly string EndPoint = "https://localhost:8081/";
#else
    private static readonly string EndPoint = "[your endpoint]";
#endif

    /// <summary>
    /// 認証キー(固定)
    /// </summary>
#if LOCAL_DB
      private static readonly string AuthKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
#else
    private static readonly string AuthKey = "[your auth key]";
#endif

    private static DocumentClient client;


    /// <summary>
    /// EvacuationFacilityInfoオブジェクトを保存します。
    /// </summary>
    public static void SaveEvacuationFacilityInfos(List<EvacuationFacilityInfo> evacuationFacilityInfos)
    {
      foreach (var info in evacuationFacilityInfos)
      {
        client.UpsertDocumentAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId), info);
      }
    }

    /// <summary>
    /// 緯度経度に対する距離範囲に存在するデータ(避難施設)を取得します。
    /// </summary>
    public static List<EvacuationFacilityInfo> Search(double longitude, double latitude, int distance)
    {
      List<EvacuationFacilityInfo> result = null;

      var query = 
        client.CreateDocumentQuery<EvacuationFacilityInfo>(
          UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId),
          new FeedOptions { MaxItemCount = -1 })
          .Where(i => i.Location.Distance(new Point(longitude, latitude)) < distance);

      result = query.ToList();

      return result;
    }

    /// <summary>
    /// データベース・コレクションの初期化を行います。
    /// </summary>
    public static void Initialize(bool initDatabase = false)
    {
      client = new DocumentClient(new Uri(EndPoint), AuthKey, new ConnectionPolicy { EnableEndpointDiscovery = false });
      if (initDatabase)
      {
        CreateDatabaseIfNotExistsAsync().Wait();
        CreateCollectionIfNotExistsAsync().Wait();
      }
    }

    /// <summary>
    /// 存在しなければデータベースを作成します。
    /// </summary>
    /// <returns></returns>
    private static async Task CreateDatabaseIfNotExistsAsync()
    {
      try
      {
        await client.ReadDatabaseAsync(UriFactory.CreateDatabaseUri(DatabaseId));
      }
      catch (DocumentClientException e)
      {
        if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
          await client.CreateDatabaseAsync(new Database { Id = DatabaseId });
        }
        else
        {
          throw;
        }
      }
      catch
      {
        throw;
      }
    }

    /// <summary>
    /// 存在しなければコレクションを作成します。
    /// </summary>
    /// <returns></returns>
    private static async Task CreateCollectionIfNotExistsAsync()
    {
      try
      {
        await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId));
      }
      catch (DocumentClientException e)
      {
        if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
          // DataType.Point に対して SpatialIndex 付加してコレクションを作成
          await client.CreateDocumentCollectionAsync(
            UriFactory.CreateDatabaseUri(DatabaseId),
            new DocumentCollection { Id = CollectionId, IndexingPolicy = new IndexingPolicy(new SpatialIndex(DataType.Point)) },
            new RequestOptions { OfferThroughput = 1000 });
        }
        else
        {
          throw;
        }
      }
    }
  }
}

DatabaseId / CollectionIdは任意ですが、ここではそれぞれ「EvacuationFacilityInfoDb」「EvacuationFacilityInfoCollection」としました。
EndPointはローカルのDocumentDB Emulatorを使用するため「https://localhost:8081/」となります。AuthKeyもEmulatorである為、固定値となります。
エミュレータではなくAzure DocumentDBを利用する場合には、適切なEndPointとAuthKeyを指定してください(あなたのAzureポータルから確認可能)。
Initialize()メソッドで接続用DocumentClientオブジェクトを作成します。引数initDatabaseがtrueの場合は、データベース・コレクションの作成も行います。
CreateCollectionIfNotExistsAsync()メソッドではコレクションを作成します。ここでポイントがあります。地理的クエリーを行う為に、インデックスポリシーとしてSpatialIndexを指定してます。
SaveEvacuationFacilityInfos()メソッドは、EvacuationFacilityInfoオブジェクト配列をDocumentDBに保存します。
Search()メソッドは、データの中から、引数で指定した緯度経度に対する距離範囲内の位置にある避難施設を検索します。
以下の様に距離範囲のクエリー条件を記述することができます。

.Where(i => i.Location.Distance(new Point(longitude, latitude)) < distance);

Microsoft.Azure.Documents.Spatial.Pointクラスのコンストラクタ引数は、(緯度, 経度)ではなく (経度, 緯度) なのでご注意を。

コンバート及びDocumentDB作成処理の呼び出し

上記で用意した各クラス・メソッドを呼び出して「XMLデータからEvacuationFacilityInfoオブジェクトに変換、さらにDocumentDBに保存」までを行うコンソールアプリ「CreateEvacuationFacilityDb」のMain()メソッドを実装します。

// CreateEvacuationFacilityDb\Program.cs
using System.Collections.Generic;

using EvacuationFacilityLib.Models;
using EvacuationFacilityLib.Repositories;

namespace CreateEvacuationFacilityDb
{
  class Program
  {
    static void Main(string[] args)
    {
      // DocumentDB初期化
      EvacuationFacilityInfoRepository.Initialize(true);
      
      // XMLをロードしてカスタムクラスコレクションに変換
      // xmlのパスは書き換えてください!
      List<EvacuationFacilityInfo> evacuationFacilityInfoList = 
        GmlToCustomFormatConvertor.Load(@"J:\Projects\Github\research\DocumentDB\GIS\P20-12_13.xml");

      // カスタムクラスコレクションをDocumentDBに保存
      EvacuationFacilityInfoRepository.SaveEvacuationFacilityInfos(evacuationFacilityInfoList);
    }

  }
}

読み込むXMLファイルのパスは皆さんの環境に合わせて書き換えてください。
CreateEvacuationFacilityDbコンソールアプリを実行すると、DocumentDBにデータが作成・格納されます。

f:id:daigo-knowlbo:20170419005131p:plain

2. Webアプリを作成する

EvacuationFacilityWebアプリを作成します。

DocumentDB接続を初期化

まずGlobal.asax.csのStartで「EvacuationFacilityInfoRepository.Initialize()」を呼び出し、DocumentClientオブジェクトを初期化します。

// EvacuationFacilityWeb\Global.asax.cs
public class MvcApplication : System.Web.HttpApplication
{
  protected void Application_Start()
  {
    // DocumentDB初期化
    EvacuationFacilityInfoRepository.Initialize();

    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
  }

コントローラークラスを実装

コントローラクラスを実装します。
検索画面表示の為のHTTP GET用Index()メソッド、「避難施設を検索」クリック時のHTTP POST用Index()メソッドを実装します。
POST時には検索結果をViewBagにしまって resultビュー を表示します。

// EvacuationFacilityWeb\Controllers\HomeController.cs
using System.Web.Mvc;

using EvacuationFacilityLib.Repositories;

namespace EvacuationFacilityWeb.Controllers
{
  public class HomeController : Controller
  {
    public ActionResult Index()
    {
      return View();
    }

    [HttpPost]
    public ActionResult Index(double latitude, double longitude, int distance)
    {
      ViewBag.EvacuationFacilities = EvacuationFacilityInfoRepository.Search(longitude, latitude, distance);

      return View("result");
    }
  }
}

ビューを実装

Index及びResultのビュー定義は以下の通りです。
実行時の緯度経度は navigator.geolocation.getCurrentPosition() で取得します。

// EvacuationFacilityWeb\Views\Home\Index.cshtml
<script type="text/javascript">
  if ("geolocation" in navigator) {
    navigator.geolocation.getCurrentPosition(function (position) {
      // POST用
      $('#latitude').val(position.coords.latitude);
      $('#longitude').val(position.coords.longitude);
      // 表示用
      $('#spanLatitude').html(position.coords.latitude);
      $('#spanLongitude').html(position.coords.longitude);

      $('#currentLocationGmapUrl').attr('href', 'https://www.google.co.jp/maps/place/' + position.coords.latitude + '+' + position.coords.longitude);
    },
    function (err) {
      alert(err.code + ': ' + err.message);
    });
  } else {
    alert('geolocation is not supported.');
}

</script>

<style>
.row {
  padding-top: 5px;
  padding-bottom: 5px;
}
</style>

@using (Html.BeginForm())
{
<div class="row">
  <div class="col-xs-2"></div>
  <div class="col-xs-8">
    近隣の避難施設を検索します。
  </div>
  <div class="col-xs-2"></div>
</div>
<div class="row">
  <div class="col-xs-5 text-right">
    現在地:
  </div>
  <div class="col-xs-7">
    緯度<span id="spanLatitude"></span> 経度:<span id="spanLongitude"></span>
    <br />
    <a id="currentLocationGmapUrl" target="_blank">地図を表示</a>
  </div>
</div>
<div class="row">
  <div class="col-xs-5 text-right">
    距離範囲:
  </div>
  <div class="col-xs-7">
      <select id="distance" name="distance">
        <option value="500">0.5 km 範囲内</option>
        <option value="1000">1 km 範囲内</option>
        <option value="3000">3 km 範囲内</option>
        <option value="5000">5 km 範囲内</option>
      </select>
  </div>
</div>
<div class="row">
  <div class="col-xs-5 text-right">

  </div>
  <div class="col-xs-7">
    <input type="hidden" id="latitude" name="latitude" />
    <input type="hidden" id="longitude" name="longitude" />
    <input type="submit" value="避難施設を検索" class="btn btn-default" />
  </div>
</div>
}
<hr />
「navigator.geolocation.getCurrentPosition()」で現在位置を取得するので、LANだとIPが降られた場所で遠くになるかも・・・

以下は resultビュー です。
検索結果はHomeControllerにより ViewBag.EvacuationFacilities にEvacuationFacilityInfo配列として格納されています。
それらのデータを一覧レンダリングします。 Google Mapsへのリンクはソースにあるように緯度・経度 をクエリーパラメータとすることで実装します。

// EvacuationFacilityWeb\Views\Home\result.cshtml
<h2>検索結果</h2>

<a href="./">戻る</a>

@if (this.ViewBag.EvacuationFacilities != null)
{
  <p>@this.ViewBag.EvacuationFacilities.Count 件</p>
  foreach (EvacuationFacilityLib.Models.EvacuationFacilityInfo facility in this.ViewBag.EvacuationFacilities)
  {
    <p>名称:@facility.Name</p>
    <p>住所:@facility.Address</p>
    <p>施設タイプ:@facility.FacilityType</p>
    <p><a target="_blank" href='@System.String.Format("https://www.google.co.jp/maps/place/{0}+{1}", facility.Latitude, facility.Longitude)'>地図を表示</a></p>
    <hr />
    
  }
}

まとめ

緯度・軽度からのクエリーというのは、なかなか色々なところで使えそうですね。
特にスマホアプリ系では利用用途が多そうに思います。スマホネイティブ(Xamarin Forms含め)なら位置情報もより正確に取得出来、マップを含めたアプリ連携がよりスムーズです。
さらに技術を進めると単純な位置関係だけでなく、道路を考慮した実移動距離となってきますが、Azureに限らずどんどん進化していくような気がしています。

ということで、今回、ソースの解説は結構 雑い かったと思いますが、一式をgithubにあげたので、そちらを見ていただければ幸いです。

Visual Studio for Mac から ASP.NET CoreアプリをAzureにパブリッシュ!

Visual Studio for MacASP.NET CoreアプリをAzure App Service にパブリッシュしてみたいと思います。

では早速。(Visual Studio for MacGUIとウィザードが自動出力するコードのみを使ったノンコーディング操作で行きます。)

1. プロジェクトの作成

Visual Studio for Macを起動し、ASP.NET Core Webアプリケーションを作成します。
メニュー「ファイル → 新しいソリューション」を選択します。
「新しいプロジェクト」ダイアログで「.NET Core → App → ASP.NET Core Web App」を選択して「次へ」をクリックします。

f:id:daigo-knowlbo:20170414014406p:plain

プロジェクト名・ソリューション名を入力して「作成」をクリックします。

f:id:daigo-knowlbo:20170414014430p:plain

カラカラとNugetパッケージの復元が行われるので、完了するまで少し待ちます・・・

2. Azureにパブリッシュ

ソリューションからプロジェクト名を右マウスクリックし「Publish → Publish to Azure」をクリックします。

f:id:daigo-knowlbo:20170414015001p:plain

パブリッシュ済みのAzure App Serviceの一覧がリストされます。
今回は新規に追加したいので左下の「ニュース」をクリックします(ニュース・・・というか 新規 だと思う・・・)。

f:id:daigo-knowlbo:20170414014505p:plain

(そして更に、また謎の日本語のダイアログが表示されます)

f:id:daigo-knowlbo:20170414014516p:plain

Android サービス」・・・実際にはAzure App Serviceが作成されるのでご心配なく・・・
「作成済み」ボタン・・・「作成」と認識してください。
(この辺りは2017/4/14時点なので、そのうち正しい日本語にローカライズされるでしょう・・・)
まあ、Azureではおなじみの アプリ名 や リソースグループ名 価格プラン 等を入力します。
ちなみに今回は「プロジェクト名」は CoreWebApp1 としたのですが、Azureにパブリッシュする「アプリ名」は RdCoreWebApp1 としています。この名称は異なっていてもOKです。

パブリッシュが完了すると、ブラウザで対象アプリが実行されます。

f:id:daigo-knowlbo:20170414020000p:plain

3. Azureポータルで確認

Azureポータルに行き、今回パブリッシュしたアプリ(リソースグループ)を確認したのが以下の画像キャプチャです。

f:id:daigo-knowlbo:20170414015938p:plain

4. まとめ

はい!
サクサクっとVisual Studio for MacからAzureにパブリッシュできました!
Visual Studio for Macが Preview から 正式版 になるのが待ち遠しいです。
そして各種環境の.NET Standard 2.0実装がリリースされれば、更にWindowsMacの敷居が低くなるでしょう^^

ASP.NET Core で URL Rewrite する

Webサーバーである「Apache」や「IIS」には URL Rewrite 機能が付いています。
同様にASP.NET Coreでも、アプリケーションレベルとしてのURL Rewrite機能が実装がされています。

ということで、簡単にご紹介を。

準備

Visual Studio 2017を起動し以下のプロジェクトを作成します。

  • 「Visual C# .NET Core → ASP.NET Core Web アプリケーション(.NET Core)」

ここではプロジェクト名は「RewriteExampleCoreWeb」としました。

f:id:daigo-knowlbo:20170401011447p:plain

f:id:daigo-knowlbo:20170401011459p:plain

以下のNugetパッケージを追加します。

f:id:daigo-knowlbo:20170401011612p:plain

Startup.csにリライト設定を追加

Startup.cs(Startupクラス)のConfigureメソッドにURL Rewrite設定を追加します。
まずは以下の using を追加しておきましょう。

using Microsoft.AspNetCore.Rewrite;

URL Rewrite設定は、以下のように RewriteOptions(Microsoft.AspNetCore.Rewrite名前空間)オブジェクト を IApplicationBuilder(app) に UseRewriter() することで設定します。
ちなみに UseRewriter() は、Microsoft.AspNetCore.Rewrite.dllアセンブリ に実装された IApplicationBuilder の拡張メソッドです。

// Startupクラス抜粋
public void Configure(
  IApplicationBuilder app, 
  IHostingEnvironment env, 
  ILoggerFactory loggerFactory)
{
  // URL Rewrite設定の基本(以下は設定が空なので実質何も起こらない)
  var options = new RewriteOptions();
  app.UseRewriter(options);
}

上記では、具体的な URL Rewrite 設定は何も行われていません。
以下にいくつかの URL Rewrite 設定の方法を紹介します。

1.シンプルにRewrite設定を追加

シンプルにRewrite設定する例を以下に示します。

  var options = new RewriteOptions()
    .AddRewrite(@"simple-rewrite-rule/(\d+)", "Rewritten/simple?id=$1", true);
  app.UseRewriter(options);

AddRewrite()メソッドを利用します。 第1引数はソースURL(正規表現を利用可能)、第2引数はリライト先URL、第3引数はパターンにマッチしたら以降のリライト設定の評価をスキップするかです(後述しますが、Add~()設定定義はチェインして定義可能です)。

また、リライト先URLの受け口となるController/RewrittenController.csを以下のように実装します。

// Controller/RewrittenController.cs
using Microsoft.AspNetCore.Mvc;

namespace RewriteExampleCoreWeb.Controllers
{
  public class RewrittenController : Controller
  {
    [HttpGet]
    public IActionResult Simple(int id)
    {
      return Content(string.Format("called RewittenControlle.Simple({0})", id));
    }
  }
}
実行

ブラウザで「http://localhost:6048/simple-rewrite-rule/456」にアクセスすると以下の実行結果が得られます。

f:id:daigo-knowlbo:20170401021400p:plain

URLリライトが動作し、RewrittenController.Simple()が呼び出されたことがわかります。

2.Apache mod_rewrite設定を追加

Apachemod_rewrite 設定ファイルを利用することも可能です。

まず、設定ファイルを用意します。ここではプロジェクトルートに「apache_mod_rewrite.txt」というファイルを追加しました。
※txtファイルの置き場所に関するセキュリティ観点はここでは考慮しませんm(_ _)m

// apache_mod_rewrite.txt
RewriteRule ^/apache-mod-rules/(.*) /rewritten/ApacheMod?id=$1

次に RewriteOptions の AddApacheModRewrite()メソッド を呼出し、Apache mod rewrite設定ファイル「apache_mod_rewrite.txt」を読み込みます。
以下の実装のように「Add~()」メソッドはチェインして重ねて定義することができます。

  var options = new RewriteOptions()
    .AddRewrite(@"simple-rewrite-rule/(\d+)", "Rewritten/simple?id=$1", true)
    .AddApacheModRewrite(env.ContentRootFileProvider, "apache_mod_rewrite.txt");
  app.UseRewriter(options);

リライト先のコントローラには ApacheMod()メソッド を追加します。

// Controller/RewrittenController.cs
using Microsoft.AspNetCore.Mvc;

namespace RewriteExampleCoreWeb.Controllers
{
  public class RewrittenController : Controller
  {
    [HttpGet]
    public IActionResult ApacheMod(int id)
    {
      return Content(string.Format("called RewittenControlle.ApacheMod({0})", id));
    }

    ... 省略
  }
}
実行

ブラウザで「http://localhost:6048/apache-mod-rules/123」にアクセスすると以下の実行結果が得られます。

f:id:daigo-knowlbo:20170401022836p:plain

3.IIS Rewrite設定を追加

IISRewrite 設定ファイルを利用することも可能です。

まず、設定ファイルを用意します。ここではプロジェクトルートに「IISUrlRewrite.xml」というファイルを追加しました。

// IISUrlRewrite.xml
<rewrite>
  <rules>
    <rule name="Rewrite segment to id querystring" stopProcessing="true">
      <match url="^iis-rules-rewrite/(.*)$" />
      <action type="Rewrite" url="Rewritten/IisRewrite?id={R:1}" appendQueryString="false"/>
    </rule>
  </rules>
</rewrite>

次に RewriteOptions の AddIISUrlRewrite()メソッド を呼出し、IIS Rewrite設定ファイル「IISUrlRewrite.xml」を読み込みます。
以下の実装のように「Add~()」メソッドはチェインして重ねて定義することができます。

  var options = new RewriteOptions()
    .AddRewrite(@"simple-rewrite-rule/(\d+)", "Rewritten/simple?id=$1", true)
    .AddApacheModRewrite(env.ContentRootFileProvider, "apache_mod_rewrite.txt")
    .AddIISUrlRewrite(env.ContentRootFileProvider, "IISUrlRewrite.xml");
  app.UseRewriter(options);

リライト先のコントローラには IisRewrite()メソッド を追加します。

// Controller/RewrittenController.cs
using Microsoft.AspNetCore.Mvc;

namespace RewriteExampleCoreWeb.Controllers
{
  public class RewrittenController : Controller
  {
    [HttpGet]
    public IActionResult IisRewrite(int id)
    {
      return Content(string.Format("called RewittenControlle.IisRewrite({0})", id));
    }

    ... 省略
  }
}
実行

ブラウザで「http://localhost:6048/iis-rules-rewrite/789」にアクセスすると以下の実行結果が得られます。

f:id:daigo-knowlbo:20170401023436p:plain

その他(リダイレクト)

上記以外に以下の設定が可能です。

  • AddRedirect()設定によるリダイレクト
  • メソッドベースルールによるリダイレクト
  • IRuleベースルールによるリダイレクト

ただしこれらは「リライト」ではなく「リダイレクト」になるため、本投稿では割愛させていただきます。

また、上記3リダイレクト方式を含んだ公式ドキュメントは以下になります。

docs.microsoft.com

C#7のローカル関数(Local Function)とは何か

C# 7には「ローカル関数(Local Function)」という機能が追加されました。

関数(メソッド)の中に関数を定義できるというものです。

以下は(処理的には何の意味も持ちませんが)ローカル関数を使った例です(リスト1)。
TestClass1クラスのTestMethod1()メソッド内に定義した「innerFunc(int n)」がローカル関数です。

// リスト1
// ローカル関数を利用
namespace TestNs
{
  public class TestClass1
  {
    public int TestMethod1(int num)
    {
      // ローカル関数
      int innerFunc(int n) {
        int i = n * 10;
        return i;
      };
      
      var result = innerFunc(num);
      return result;
    }
  }
}

ローカル関数を利用する1つのケースは、「特定のメソッド内からしか呼び出されない処理をローカル関数として定義する」ことです。
privateメソッドとして切り出しても良いのですが、同クラス内の別メソッドから予期せず呼び出されるリスクを持ちます。
ローカル関数を使用しない(privateメソッドに切り出す)実装は以下の通りです(リスト2)。

// リスト2
// 従来の実装
namespace TestNs
{
  public class TestClass2
  {
    public int TestMethod2(int num)
    {
      var result = this.InnerFunc(num);
      return result;
    }

    // TestMethod2からしか呼び出されないメソッド
    private int InnerFunc(int n)
    {
      int i = n * 10;
      return i;
    }
  }
}

MSILはどうなっているのか?

では、視点を変えまして・・・
リスト1をコンパイルした「MSIL(Microsoft Intermediate Language)」は一体どういったものでしょうか?
ildasm.exeを使って、リスト1をコンパイルしたアセンブリ(DLL)を見てみましょう。
TestMethod1()のMSILは以下の通りです(リスト3)。

// リスト3
.method public hidebysig instance int32  TestMethod1(int32 num) cil managed
{
  // Code size       16 (0x10)
  .maxstack  1
  .locals init ([0] int32 result,
           [1] int32 V_1)
  IL_0000:  nop
  IL_0001:  nop
  IL_0002:  nop
  IL_0003:  ldarg.1
  IL_0004:  call       int32 TestNs.TestClass1::'<TestMethod1>g__innerFunc1_0'(int32)
  IL_0009:  stloc.0
  IL_000a:  ldloc.0
  IL_000b:  stloc.1
  IL_000c:  br.s       IL_000e
  IL_000e:  ldloc.1
  IL_000f:  ret
} // end of method TestClass1::TestMethod1

ローカル関数を「TestNs.TestClass1::‘ginnerFunc1_0'」として呼び出しています。
「TestNs.TestClass1::’g
innerFunc1_0'」実装のILは、以下の通りです(リスト4)。

// リスト4
.method assembly hidebysig static int32  '<TestMethod1>g__innerFunc1_0'(int32 n) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       12 (0xc)
  .maxstack  2
  .locals init ([0] int32 i,
           [1] int32 V_1)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldc.i4.s   10
  IL_0004:  mul
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  stloc.1
  IL_0008:  br.s       IL_000a
  IL_000a:  ldloc.1
  IL_000b:  ret
} // end of method TestClass1::'<TestMethod1>g__innerFunc1_0'

staticメソッドとして定義されています。またpublic / private等のアクセス修飾子がついていません。つまり internal なメソッドということです。
上記より、リスト4は 以下のC#実装と同意となります(リスト5)。

// リスト5
namespace TestNs
{
  public class TestClass1
  {
    public int TestMethod1(int num)
    { 
      var result = TestClass1.innerFunc(num);
      return result;
    }

    internal static int innerFunc(int n)
    {
      int i = n * 10;
      return i;
    }
  }
}

ローカル関数は「internal staticメソッド」と解釈された

つまりC#コード上での ローカル関数 は、C#コンパイラを通してMSILとなる段階で「internal staticメソッド」として解釈されていました。

が!!!!!

必ずしも「internal staticメソッド」となるわけではありません。

ローカル関数がインスタンスプロパティを参照している場合

はい、ということで以下のようなケースではどうでしょうか?(リスト6)

// リスト6
namespace TestNs
{
  public class TestClass1
  {
    private int propValue = 111;

    public int TestMethod1(int num)
    {
      // ローカル関数
      int innerFunc(int n) {
        // インスタンスプロパティを参照している
        int i = n * this.propValue;
        return i;
      };
      
      var result = innerFunc(num);
      return result;
    }
  }
}

先程と同様にリスト6をコンパイルしたアセンブリ(DLL)のinnerFunc()部のMSILを確認してみます(リスト7)。

// リスト7
.method private hidebysig instance int32 
        '<TestMethod1>g__innerFunc1_0'(int32 n) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       16 (0x10)
  .maxstack  2
  .locals init ([0] int32 i,
           [1] int32 V_1)
  IL_0000:  nop
  IL_0001:  ldarg.1
  IL_0002:  ldarg.0
  IL_0003:  ldfld      int32 TestNs.TestClass1::propValue
  IL_0008:  mul
  IL_0009:  stloc.0
  IL_000a:  ldloc.0
  IL_000b:  stloc.1
  IL_000c:  br.s       IL_000e
  IL_000e:  ldloc.1
  IL_000f:  ret
} // end of method TestClass1::'<TestMethod1>g__innerFunc1_0'

メソッドの実装が「internal static」から「private」に変わりました。
そう、ローカル関数内の実装が「クラスのインスタンスプロパティを参照する実装」に変わった為、ローカル関数はインスタンスメソッドとして解釈されるようになりました。

まとめ

C# 7のローカル関数とは「C#言語の仕様 」あり「MSILレベルの仕様ではない」ということです。
C#コンパイラによって解釈され、MSILでは従来通りの.NETの実装となる。
なんか、ネガティブっぽい言い方になっていますが、最近のC#言語仕様の多くは(ほとんどは?)、C#コンパイラが解釈する仕様となっています。dynamicなんかはすごいMSILに解釈されますしね。

ということで、「知っても知らなくてもアプリは作れるよ!」な内容の投稿でした^^;

※本投稿では ildasm.exe を使って MSIL を直接確認しましたが、Reflectorなんかを使って C#コードに逆コンパイルしたコードを確認するとより読みやすいと思います。

Azure FunctionsをVS2017でローカル実行&デバッグしてパブリッシュする

MSDNブログ(↓↓↓)で Azure Functions 関連の記事を見たので自分でやってみました。

blogs.msdn.microsoft.com

上記ブログに記述されている事、そして今回自分で試した事を要約すると以下の内容です。

  1. Azure FunctionsをVisual Studio 2017で開発&デバッグする(つまりローカル実行するということ)
  2. Azure Functionsをクラスライブラリとしてビルド&パブリッシュする

1 は、まあ 開発効率を上げるということですね。
Azure Functions自体はAzureポータルでコーディングできますが、本格的実装はローカルで行いたいですよね。

2 は、クラスライブラリとしてアセンブリ(dll)にしてデプロイすることで実行パフォーマンスを上げる意味を持ちます。
Azure Functionsは5分間非アクティブであるとアイドル状態となり、次の実行時には再度メモリ上に展開されます。C# / JavaScriptコードがデプロイされている場合、アセンブリへのコンパイル作業から再実行され起動パフォーマンスが落ちてしまいます。

では、以下に手順を・・・

ちなみに、Visual Studioとの統合ツール「Visual Studio Tools for Azure Functions」がまだ 2017 対応できていないらしく、そのため以下のような手動による操作がいくつか発生します。でも、あと数週間もすればツールがリリースされるみたいです。

Azure Functions CLI のインストール

Azure Functions CLI をインストールします。
以下のnpmコマンドでグローバル領域にインストールします。

npm i -g azure-functions-cli

www.npmjs.com

Visual Studio 2017 でWebプロジェクトを作成

Visual Studio 2017を起動します。
メニュー「ファイル → 新規作成 → プロジェクト」を選択します。
プロジェクトテンプレートは「Web → ASP.NET Web アプリケーション(.NET Framework)」、プロジェクト名はここでは「AzureFunctionsWeb」としました。

f:id:daigo-knowlbo:20170323130918p:plain

「空」テンプレートを選択します。

f:id:daigo-knowlbo:20170323130947p:plain

プロジェクトのプロパティを表示します(ソリューションエクスプローラからプロジェクトをマウス右ボタンクリックメニューで「プロパティ」を選択)。

f:id:daigo-knowlbo:20170323131619p:plain

Webタブを選択し、以下の設定を行います。
* 「外部プログラムを起動する」を選択。
* パスは「C:\Users\「【ユーザー名】\AppData\Roaming\npm\node_modules\azure-functions-cli\bin\func.exe」とする。
* 「コマンドライン引数」は「host start」とする。
* 「作業ディレクトリ」は【プロジェクトフォルダ】とする。

設定を行った画面は以下の通り。

f:id:daigo-knowlbo:20170323132050p:plain

Azure Portal上で Azure Functions を作成

Visual Studio Tools for Azure Functionsがリリースされれば、VSからプロジェクトを生成できると思いますが、それが今はないのでポータルで作成して、ローカルVSにベースとなるソースを落としてきます)

Azure Functions 作成画面を表示

https://functions.azure.com/signinをブラウザで開きます。
「New function app」の「Name」に作成するファンクション名を入力します。ここでは「RyuichiFunction0916」としました。
「Region」は「Japan East」としました。
「Create + get started」をクリックします。

f:id:daigo-knowlbo:20170323192122p:plain

「WebHook + API」「C#」を選択し、「この関数を作成する」ボタンをクリックします。

f:id:daigo-knowlbo:20170323192411p:plain

作成完了。

f:id:daigo-knowlbo:20170323192506p:plain

Azure Functions ソースのダウンロード

左下の「Function App の設定」をクリックします。

f:id:daigo-knowlbo:20170323192544p:plain

「Kuduに移動」をクリックします。

f:id:daigo-knowlbo:20170323192731p:plain

「site」を選択します。

f:id:daigo-knowlbo:20170323192817p:plain

「wwwroot」のダウンロードアイコンをクリックします。

f:id:daigo-knowlbo:20170323193106p:plain

ダウンロードしたzipのイメージは以下の通り。

f:id:daigo-knowlbo:20170323193250p:plain

WebプロジェクトにFunctionsソースを追加

先程作成したWebプロジェクト(AzureFunctionsWeb)フォルダにzipの内容を解凍します。

f:id:daigo-knowlbo:20170323193453p:plain

追加のファイルをプロジェクトに含めます。
また「run.csx」ファイルを「run.cs」にリネームします。

f:id:daigo-knowlbo:20170323193742p:plain

run.csを以下のように修正します。
run.csxでは単純に run() だけが定義されていますが、csコードにする為「名前空間・クラス」でくくります。
また必要な using を追加します。
レスポンス文字列も若干修正を加えました。

// run.cs
using Microsoft.Azure.WebJobs.Host;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

namespace AzureFunctionsWeb
{
  public class ExampleFunction
  {
    public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
    {
      log.Info("C# HTTP trigger function processed a request.");

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

      // Get request body
      dynamic data = await req.Content.ReadAsAsync<object>();

      // Set name to query string or body data
      name = name ?? data?.name;

      return name == null
        ? req.CreateResponse(HttpStatusCode.BadRequest, 
                             "Please pass a name on the query string or in the request body")
        : req.CreateResponse(HttpStatusCode.OK,
                             "Hello " + 
                              name + 
                              ". I'm assembly!!");
    }
  }
}

プロジェクトにNugetパッケージを追加

Nugetで以下のパッケージを追加します。

function.jsonの調整

function.jsonに「scriptFile」「entryPoint」キーを追加します。
scriptFileにはアセンブリ名を、entryPointには名前空間/クラス付きのRun()メソッドの完全名を記述します。

// function.json
{
  "scriptFile": "..\\bin\\AzureFunctionsWeb.dll",
  "entryPoint": "AzureFunctionsWeb.ExampleFunction.Run",
  "disabled": false,
  "bindings": [
    {
      "authLevel": "function",
      "name": "req",
      "type": "httpTrigger",
      "direction": "in"
    },
    {
      "name": "$return",
      "type": "http",
      "direction": "out"
    }
  ]
}

ローカル実行

Visual StudioでF5クリックにより実行を行います。
以下のようなコマンドプロンプトが表示され Function がリクエスト待ち(実行待ち)状態となります。

f:id:daigo-knowlbo:20170323195843p:plain

コマンドプロンプト表示上に、以下の記述を確認することができます。

Job host started
Http Function HttpTriggerCSharp1: http://localhost:7071/api/HttpTriggerCSharp1

このURLがFunctionの受け口のURLになります。

サンプル(ExampleFunction)は「name」というキーをパラメータとして受け取ります。その為、以下のURLをブラウザでアクセスしてみましょう。

http://localhost:7071/api/HttpTriggerCSharp1?name=daigo

Functionが実行され以下のようなレスポンスが得られます。

f:id:daigo-knowlbo:20170323200130p:plain

ブレークポイントもきちんと止まります。

f:id:daigo-knowlbo:20170323200300p:plain

Azure FunctionsへのPublish

次に、ローカルで実装を行った Function を AzureにPublishします。
コマンドプロンプトを起動します。

作成したWebプロジェクトフォルダ(F:\Projects\Research\AzureFunctionsWeb\AzureFunctionsWeb)に移動します。

以下のコマンドを実行してCLIからAzureにログインします。

func azure login  

Azureへのログイン画面が表示されるので、ログインを行います。

f:id:daigo-knowlbo:20170323200613p:plain

以下のコマンドで、現在PublishされているFunctionの一覧を確認します。

func azure functionapp list

現在の私の環境では2つのFunctionがPublishされていました。
RyuichiFunction0916の方が、今回作成したものです。

f:id:daigo-knowlbo:20170323200744p:plain

以下のコマンドによりFunctionをローカルからAzureへPublishします。
(RyuichiFunction0916部分は適時 Function 名に置き換えます)

func azure functionapp publish RyuichiFunction0916

正常にPublishされた結果画面が以下になります。

f:id:daigo-knowlbo:20170323200919p:plain

Azure Functionsを実行

Azureポータルで「RyuichiFunction0916」を表示。
「開発」タブを選択、「テスト」タブを選択、「要求本文」をテスト文字列に修正、「実行ボタン」クリック、を行います。
以下のように「出力」に「"Hello Daigo. I’m assembly!!“」が表示されました。
VS2017上で実装したロジックが実行されていることが確認できます。

f:id:daigo-knowlbo:20170323202035p:plain

まとめ

VS2017用「Visual Studio Tools for Azure Functions」がリリースされれば、上記のようなゴニョゴニョした作業はバックエンドで自動化されると思います。ただ、CLIなんかで、裏の動きを知っておくのは良いことだと思っています。
また、Azure functionsやマイクロサービスについては、私自身はまだ実運用の場面では使ったことがないので、色々な知見を経験者の方から学びたいと思っております。
WebHookを絡めてGithubだったりSlackだったりと連携する実装なんかも比較的容易に出来るようなので、色々試してみたいと思います。