Visual StudioとAzure Functionsで作るLINE BOTアプリケーション入門

以前の記事で紹介した LINE BOT開発用のAzure Functionsプロジェクトテンプレートですが、 サンプルコードを大幅に追加しました。
(詳しくはMarketplaceのRelease Notesを確認してみてください。)

marketplace.visualstudio.com

この機会に是非、使ってほしい!ということで、今回はLine.Messagingライブラリを使ったAzure FunctionsのLINE BOTアプリケーション開発について少し解説してみたいと思います。

目次

Visual Studio とAzuzre Functionsで作るLINE BOTアプリケーション入門

はじめに

ここで紹介するサンプルコードは、拙作Line.Messagingクラスライブラリを使用することを前提としています。

www.nuget.org

この記事の内容を実際に試したい方は、MarketPlaceに記載のクイックスタートガイド参考に開発環境を作っておきましょう。
(導入時の問題、質問等ありましたら、このブログのコメント欄でも、MarketplaceのQ&AでもTwitterでも何でも良いのでご連絡ください)

Webhook イベントを処理する

友だち追加やメッセージ送信などのイベントは、指定したAzure FunctionsのURLにHTTP POSTリクエストとして送信されます。 そのリクエストによって、HttpTriggerFunctionクラスのRunメソッドが実行されます。

以下は、Line.Messagingを使用したHttpTriggerFunctionの実装例です。

public static class HttpTriggerFunction
{
    //LINE Messaging API クライアントの初期化
    static LineMessagingClient lineMessagingClient;
    static HttpTriggerFunction()
    {
        lineMessagingClient = new LineMessagingClient(System.Configuration.ConfigurationManager.AppSettings["ChannelAccessToken"]);
        var sp = ServicePointManager.FindServicePoint(new Uri("https://api.line.me"));
        sp.ConnectionLeaseTimeout = 60 * 1000;
    }

    [FunctionName("LineMessagingApiSample")]
    public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)]HttpRequestMessage req, TraceWriter log)
    {
        //WebhookのリクエストBodyからイベントオブジェクトを取得
        IEnumerable<WebhookEvent> events;
        try
        {
            var channelSecret = System.Configuration.ConfigurationManager.AppSettings["ChannelSecret"];
            events = await req.GetWebhookEventsAsync(channelSecret);
        }
        catch (InvalidSignatureException e)
        {
            return req.CreateResponse(HttpStatusCode.Forbidden, new { Message = e.Message });
        }

        //イベントを処理する
        try
        {
            var app = new EchoBotApp(lineMessagingClient,log);
            await app.RunAsync(events);
        }
        catch (Exception e)
        {
            log.Error(e.ToString());
        }
        return req.CreateResponse(HttpStatusCode.OK);
    }
}

LINE Messaging APIクライアントを初期化する

メッセージのリプライ、プッシュ通知などのLINEサーバーへのリクエストにはLine.Messaging.LineMessagingClientクラスを使用します。 このクラスは内部でSystem.Net.Http.HttpClientクラスを使用しています。
これをHttpTriggerFunctionクラスのStaticフィールドに1インスタンスだけ作成し、使いまわすようにします。

インスタンスを使いまわす理由は、以下を参照ください。

リクエストBodyからWebhook Event Objectを取得する

Webhookイベントの内容は、リクエストBodyにJSONで格納されています。
Line.Messagingライブラリでは、Run関数の引数HttpRequestMessage reqからEvent Objectを取得する拡張メソッドを用意しています。

static Task<IEnumerable<WebhookEvent>> GetWebhookEventsAsync(this HttpRequestMessage req,strin channelSecret);

このメソッドは以下の処理を行います。

  • リクエストの構文検証(Signature Validation)を行い、NGの場合は例外を投げる
  • リクエストBodyのJSONをパースしてWebhook Event Object(のコレクション)を返す
[FunctionName("LineMessagingApiSample")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)]HttpRequestMessage req, TraceWriter log)
{
    IEnumerable<WebhookEvent> events;
    try
    {
        var channelSecret = System.Configuration.ConfigurationManager.AppSettings["ChannelSecret"];
        //Webhook Event Objectの取得
        events = await req.GetWebhookEventsAsync(channelSecret);
    }
    catch (InvalidSignatureException e)
    {
        //構文検証エラー
        return req.CreateResponse(HttpStatusCode.Forbidden, new { Message = e.Message });
    }

    //...
}

Webhook Event Object

Webhook Event Object はイベントのタイプ別に7種類用意されています。
Line.Messagingライブラリでは、WebhookEvent抽象クラスのサブクラスとして実装されています。

public abstract class WebhookEvent
{
    public WebhookEventType Type { get; }
    public WebhookEventSource Source { get; }
    public long Timestamp { get; }
}
  • MessageEvent
    ユーザーからメッセージが送られた際に発行されるイベント。
    メッセージには、送られてくるデータの種類によって異なる以下のタイプがあります。

    • text (文字列メッセージ)
    • image(画像データ)
    • audio(オーディオデータ)
    • video(ビデオデータ)
    • file(ファイル)
    • location(位置情報)
    • sticker(スタンプ
  • FollowEvent
    ユーザーがBOTアカウントをフォローした際に発行されるイベント

  • UnfollowEvent
    ユーザーにBTOアカウントをブロックされた際に発行されるイベント
  • JoinEvent
    BOTアカウントがグループやトークルームに参加した際に発行されるイベント
  • LeaveEvent
    BOTアカウントがグループやトークルームから退室させられた際に発行されるイベント
  • PostbackEvent
    テンプレートメッセージに設定したPostbackアクションが実行された際に発行されるイベント
  • BeaconEvent
    LINE Beaconデバイスの受信圏内に入った(出た)際に発行されるイベント

※各イベントの詳細はLINE公式のAPIリファレンスを参照ください。

WebhookEventSource

WebhookEventクラスは、イベントの送信元を表すSourceプロパティを持ちます。

Line.MessagingライブラリではWebhookEventSourceクラスとして実装しています。

public class WebhookEventSource
{
    public EventSourceType Type { get; }
    public string Id { get; }        
    public string UserId { get; }
}

送信元のタイプは以下の3種類です。

  • User (ユーザーと1対1のトークで発生するイベントに付与されます)
  • Group (トークグループで発生するイベントに付与されます)
  • Room (トークルーム(複数ユーザー間でのトーク)で発生するイベントに付与されます)

また、Idプロパティには、各送信元を示すIDが格納されます(User ID、Group ID、Room ID)。
UserIdには、各送信元から発信したユーザーのIDが入ります。

イベントを処理する

あとは、リクエストBodyから取得したWebhookEventオブジェクトの内容を見てそれに応じた処理を記述すればOKです。
これをHttpTriggerFunctionクラスに直接記述しても良いのですが、Line.Messagingライブラリではイベントの種類に応じて処理を振り分けて実行するためのクラスが用意されているので、これを使います。

WebhookApplicationクラスを使用する

WebhookApplicationクラスは、RunAsyncというメソッドでWebhookEventのコレクションを受け取り、イベントの種類に応じて各イベントの処理(On~ 仮想メソッド)を呼ぶだけのクラスです。

public abstract class WebhookApplication
{
    protected virtual Task OnMessageAsync(MessageEvent ev) => Task.CompletedTask;
    protected virtual Task OnJoinAsync(JoinEvent ev) => Task.CompletedTask;
    protected virtual Task OnLeaveAsync(LeaveEvent ev) => Task.CompletedTask;
    protected virtual Task OnFollowAsync(FollowEvent ev) => Task.CompletedTask;
    protected virtual Task OnUnfollowAsync(UnfollowEvent ev) => Task.CompletedTask;
    protected virtual Task OnBeaconAsync(BeaconEvent ev) => Task.CompletedTask;
    protected virtual Task OnPostbackAsync(PostbackEvent ev) => Task.CompletedTask;

    public async Task RunAsync(IEnumerable<WebhookEvent> events)
    {
        foreach (var ev in events)
        {
            switch (ev.Type)
            {
                case WebhookEventType.Message:
                    await OnMessageAsync((MessageEvent)ev).ConfigureAwait(false);
                    break;
                case WebhookEventType.Join:
                    await OnJoinAsync((JoinEvent)ev).ConfigureAwait(false);
                    break;
                case WebhookEventType.Leave:
                    await OnLeaveAsync((LeaveEvent)ev).ConfigureAwait(false);
                    break;
                case WebhookEventType.Follow:
                    await OnFollowAsync((FollowEvent)ev).ConfigureAwait(false);
                    break;
                case WebhookEventType.Unfollow:
                    await OnUnfollowAsync((UnfollowEvent)ev).ConfigureAwait(false);
                    break;
                case WebhookEventType.Postback:
                    await OnPostbackAsync((PostbackEvent)ev).ConfigureAwait(false);
                    break;
                case WebhookEventType.Beacon:
                    await OnBeaconAsync((BeaconEvent)ev).ConfigureAwait(false);
                    break;
            }
        }
    }
}

このクラスを継承したクラスを作成し、必要な仮想メソッドをオーバーライドして処理を記述します。
以下に、ユーザーのメッセージをオウム返しするだけの処理の実装例を示します。

//オウム返しBOTアプリケーション
public class EchoBotApp:WebhookApplication
{
    private LineMessagingClient MessagingClient {get;}
    private TraceWriter Log{get;}

    //コンストラクタでLineMessagingClientのインスタンスを渡す
    public EchoBotApp(LineMessagingClient messagingClient, TraceWriter log)
    {
        MessagingClient = messagingClient;
        Log = log;
    } 

    //メッセージEventを処理する
    protected override async Task OnMessageAsync(MessageEvent ev)
    {
        //Textメッセージにだけ返信する
        if(ev.Type != EventMessageType.Text){return;}

        return MessagingClient.ReplyMessageAsync(ev.ReplyToken, ((TextEventMessage)ev.Message).Text);
    }
}

あとは、HttpTriggerFunctionのRunメソッドで以下の様に記述するだけです。

//WebhookApplicationを継承したクラスでイベントを処理する
var app = new EchoBotApp(lineMessagingClient,log);
await app.RunAsync(events);

まとめ

基本的な使い方は以上です。
次回からは、もう少し具体的な実装例を解説してみたいと思います。

LINE BOT 開発に使えるLINE Messaging API の.Net Standard Libraryと、Visual Studio 2017用プロジェクトテンプレートを作りました

LINEでビンゴゲームができるBOTを作ったのですが、 このBOTで実装したMessaging API のコードをクラスライブラリに切り出してみました。

pierre3.hatenablog.com

Line.Messaging V0.20-alpha

NuGet Galleryで公開しています。

www.nuget.org

  • ターゲットバージョンは.Net Standard 1.3 としました。
  • APIは一通り実装されていますが、テストが不十分のためAlphaリリース版としています。

LINE BOT Function プロジェクトテンプレート

Azure Functionsのプロジェクトに、「Line.MessagingのNuGet参照」と「ユーザーのメッセージを受け取り、オウム返しをするコード」を追加したテンプレートです。

Visual Studio Marketplaceからダウンロードできます。
テンプレートの使い方は、以下のリンク先に記載されています。

marketplace.visualstudio.com

これからLINE BOT を作ってみたいという方は、ぜひこのテンプレートをダウンロートして試してみてください。

GitHubリポジトリ

ソースコード等はこちらから github.com

LINE でビンゴゲームができるBOTを改良しました

BINGO BOT

以前に紹介した、LINE BOTを改良しました。

pierre3.hatenablog.com

改良したところ

  • リッチメニューに対応しました
  • ビンゴカードを画像で取得できるようになりました
  • ゲーム参加者同士の簡易チャットが可能になりました

リッチメニューに対応しました。

f:id:pierre3:20170809224836p:plain:w300

画面下部のメニューバーに追加した「BINGO Menu」をタップすると、上図の様にメニューが表示されます。 メニューの各項目をタップすると、その項目に割り当てられた定型文が送信されます。

メニュー 定型文(コマンド) 説明
ゲームを開始する 開始 新しいゲームを開始します。 このコマンドでゲームを開始したユーザーがゲームの進行役になります。
ゲームに参加する 参加 ゲームの進行役が作成したゲームに参加します。
番号を引く ドロー 番号を引いてゲームを進めます。 ゲームの進行役のユーザーのみ使用可能です。
カードを更新 カード ゲームの進行状況を反映した、最新のカード画像を取得します。ゲームの参加者のみ使用可能です。

f:id:pierre3:20170811075116p:plain:w300

ビンゴカードを画像で取得できるようになりました

f:id:pierre3:20170809225012p:plain:w300

これだけで、だいぶビンゴゲームらしくなりましたね。(デザインはいまいちですが)

ゲーム参加者同士の簡易チャットが可能になりました

これまで、ゲームの進行に必要なキーワード(開始、参加、ドロー、カード、終了)のみを受け付けて、その他のメッセージは無視していました。
今回、上記キーワード以外のメッセージは、同じゲームに参加している全ユーザーに送信するようにしました。

f:id:pierre3:20170809225343p:plain:w300 f:id:pierre3:20170809225518p:plain:w300

上図のように他のユーザーからのメッセージには、先頭に@ユーザー名を付けて送信します。

お試しユーザー募集中!

ちょっと試してみたいと思った方、こちらから友達登録してみてください!

友だち追加

LINE BOT Webhooks の署名検証をC#で実装する

LINE BOT 開発では、ユーザーが送信したメッセージなどのイベントは、指定したURLにHTTPSのPOSTリクエストとして送信されますが、リクエストの送信元が間違いなくLINEからのものであることを確認する必要があります。

LINE API Referenceには以下の様に書かれています。

Signature validation

リクエストの送信元がLINEであることを確認するために署名検証を行わなくてはなりません。 各リクエストには X-Line-Signature ヘッダが付与されています。 X-Line-Signature ヘッダの値と、request body と Channel secret から計算した signature が同じものであることをリクエストごとに 必ず検証してください。

検証は以下の手順で行います。

  1. Channel secretを秘密鍵として、HMAC-SHA256アルゴリズムによりrequest bodyのダイジェスト値を得る。
  2. ダイジェスト値をBASE64エンコードした文字列が、request headerに付与されたsignatureと一致することを確認する。

今回は、これをC#で実装してみましょう。

必要なデータは以下の3つです。

  • リクエストのX-Line-Signature ヘッダに格納されている値
  • リクエストBODYの値
  • Bot アカウントに付与されるChannel Secret(Line DeveloppersのChannels>>Basic Informationで確認できます)

これらを引数にして検証を行う関数を作ります。

using System.Security.Cryptography;
using System.Text;

public bool VerifySignature(string xLineSignature, string requestBody, string channelSecret)
{
    try
    {
        //channnel secret と request body をByte配列に変換する
        var key = Encoding.UTF8.GetBytes(channelSecret);
        var body = Encoding.UTF8.GetBytes(requestBody);
        
        //channel secretをキーにしてHMAC-SHA256アルゴリズムでrequest bodyのダイジェスト値を得る
        using (HMACSHA256 hmac = new HMACSHA256(key))
        {
            var hash = hmac.ComputeHash(body, 0, body.Length);
            //ダイジェスト値をBASE64に変換
            var hash64 = Convert.ToBase64String(hash);
            //X-LINE-Signatureヘッダの値と一致すればOK!!
            return xLineSignature == hash64;
        }
    }
    catch
    {
        return false;
    }
}

これを、Azure Functions(HttpTrigger)で使用する例を以下に示します。

using System.Configuration;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
    //Channel Secretの取得。Channel SecretはAzure ポータルでアプリケーション設定に登録しておきます。
    var channelSecret = ConfigurationManager.AppSettings["ChannelSecret"]);
    //Request Body の取得
    var contentJson = await req.Content.ReadAsStringAsync();
    //X-Line-Signatureヘッダ値の取得
    var xLineSignature = req.Headers.GetValues("X-Line-Signature").FirstOrDefault()
        
    if (string.IsNullOrEmpty(xLineSignature) ||
        !VerifySignature(xLineSignature, contentJson, channelSecret))
    {
        //検証に失敗
        return req.CreateResponse(HttpStatusCode.Forbidden, new { Message = "Signature validation faild." });
    }

    //BOTでやりたい処理...
}

LINEでビンゴゲームができるBOTを作りました

ビンゴゲームの基本機能をASP.NETのWeb APIで、LINEとの連携部分(BOT)をAzure Functionsで作りました。

例によってソースコードGitHubにて公開していますので、覗いてみてください。

github.com

BINGO Web API

ビンゴゲームの基本的な機能は、Azure App ServiceでホストしたASP.NET CoreのWeb APIで提供します。

次のような、ごく基本的な機能のみをREST APIで提供しています。

  • ゲームを作成する
  • ゲームにカードを追加する
  • 番号を引いてゲームを進める
  • 現在のゲームの状態を取得する
  • 現在のカードの状態を取得する

詳しくは、Swaggerさんが生成してくれたAPIリファレンスをご確認ください。 http://bingowebapi.azurewebsites.net/swagger/

LINE BOT Function

LINE BOT 関連の機能は、Azure Functionsで処理しています。

主に次のような処理を行っています

  • LINEユーザーからのメッセージを受け取る
  • メッセージの内容に応じてゲームの開始、参加、進行などを行う。
  • 状況に応じたBINGO Web APIを呼び出す。
  • ゲームの進行状況、その他メッセージを LINEユーザーに送信する

開発環境、Frameworkとか

BINGO WebAPI

Web APIの開発にはVisual Studio 2017を使用しています。
ASP.NET Core Webアプリケーション」のプロジェクトテンプレートを使用して作成しました。

DB関連はAzure SQL Server + EntityFramework Coreを使用しています。

LINE BOT Function

Azure Function Tools for Visual Studio 2017

LINE BOTの開発には,「Azure Function Tools for Visual Studio 2017」というツールを使用しています。

このツールを使用するとAzure Functions の開発がとてもに楽になります。

※ このツールは(2017年7月現在では) プレビュー版のVisual Studio (Visual Studio 2017 Preview(2))でないとインストールできないようです。 お試しする場合は、ご注意を。

Azure Table Strage

ゲームに参加しているLINEのユーザー情報と、BINGO APIの発行するゲームID、カードIDとの紐づけなどの情報はAzure Table Strageに保存しています。

遊び方

LINE BOTを友達登録します

BINGO BOTというアカウントを友達登録するとお試しで遊ぶことができます。

BINGO BOT

友だち追加

※ ただし、開発中のお試し版であることをご承知の上でご利用ください。 また、予告なくサービスが停止したり、仕様が変更されたりする場合がありますので予めご了承ください。

ゲームオーナーとプレーヤー

BINGOゲームを始めるには、ゲームを作成・進行するゲームオーナー1人と、ゲームに参加するプレーヤー(複数人)が必要です。

ゲームオーナーの操作

ゲームを作成する

まずは、ゲームオーナーがゲームを作成します。
やり方は簡単で、BINGO BOT に「0」または「開始」と送るだけです。

合言葉を決める

するとLINE BOTから、合言葉の設定を促すメッセージが帰ってきます。 合言葉は、プレーヤーがゲームに参加するために必要なキーワードです。
20文字まで好きな言葉を返信してください。 合言葉を設定しないこともできます。(その場合は「なし」と返します)

ゲームIDと合言葉をプレーヤーに伝える

合言葉を送信すると、ゲームが開始されLINE BOT からゲームIDが返信されます。
このゲームIDと、指定した合言葉を参加するプレーヤーに伝えて、入力してもらいます。

番号を引く

あとは、適当な文字をBINGO BOT に送信するだけです。 1回送信するたびに1つ番号を引きます。

ゲームを終了する

ゲーム進行中に「終了」と入力するとゲームを終了できます。(終了するまで次のゲームを遊ぶことができませんので注意)

f:id:pierre3:20170718230822p:plain:w300 f:id:pierre3:20170718231038p:plain:w300 f:id:pierre3:20170718231049p:plain:w300 f:id:pierre3:20170718231102p:plain:w300

プレーヤーの操作

ゲームに参加する

ゲームに参加するには、BINGO BOTに「1」または「参加」と送ります。

ゲームIDと合言葉を入力する

するとLINE BOTからゲームIDと合言葉を入力するよう促すメッセージが返ってくるので、ゲームオーナーから伝えられたそれらを入力します。

カードを取得する

ゲームへの参加に成功すると、BINGOカードが返信されます。
あとは、ゲームオーナーが番号を引くたびに、BINGO BOTから引いた番号が通知されます。
ゲーム進行中、何かメッセージを送ると、ヒットした番号を反映した現在のカードが取得できます。

f:id:pierre3:20170718231127p:plain:w300 f:id:pierre3:20170718231150p:plain:w300 f:id:pierre3:20170718231201p:plain:w300 f:id:pierre3:20170718231205p:plain:w300 f:id:pierre3:20170718231213p:plain:w300

まとめ

まだまだ作り込みが足りていないので、もうしばらくはこいつの開発で楽しめそうです。
とりあえず、BINGOカードは画像にしたいですね。  

あと、今後は開発中にハマったこととか、調べたことなどTIPS的な小ネタを記事にできればいいな。

Azure App Service の継続的配信(プレビュー)を試してみました

Azure App Service の継続的配信(プレビュー)

最近あまり触れていなかったASP.NET系の技術にそろそろじっくり取り組んでみようかな、と思う今日この頃。
ASP.NET Core & Azure App Service で何か作ろうとAzureポータルを触っていると、App Serviceのメニューに「継続的配信(プレビュー)」なる項目を見つけました。
今回はこれを試してみたいと思います。

f:id:pierre3:20170402083040p:plain

どんな機能か

Visual Studio Team Service (VSTS)を利用して、Azure App Serviceへの継続的配信(Continuous Delivery)を行うためのワークフローを構成してくれる機能です。

これを使用することで、GitのリポジトリにコードをPushするだけでVSTSのビルドタスクが走り、コードの取得→ビルド→Unitテスト→デプロイ といった一連の流れを自動化できるようになります。

配信元のソースはVSTSのGit リポジトリ以外にもGitHubリポジトリが指定できます。(その他、GitやSVNのプライベートなリポジトリを指定することもできるようです。)

事前準備

では、早速試してみましょう。今回はGitHubリポジトリと連携させてみます。

サンプルアプリケーション

今回サンプルとして使用するアプリケーションは、Visual Studio 2017のASP.NET Coreのプロジェクトテンプレートを使用して作成したものを使用します。

f:id:pierre3:20170402093448p:plain

  • [ファイル]-[新規作成]-[プロジェクト]で「ASP.NET Core Webアプリケーション(.NET Core)」を選択します。
  • ターゲットフレームワークに「ASP.NET Core 1.1」、テンプレートは「Web API」としました。
  • 「Dockerサポートを有効にする」なるチェックボックスがありますが、今回はOFFで作成します。

こうして作成したWebAPIのアプリケーションを、そのままGitHubに上げておきます。

github.com

VSTSでプロジェクトを作成

Visual Studio Team Service(VSTS)で、このアプリケーション用のプロジェクトを予め作成しておく必要があります。

VSTSのアカウントがない場合は、以下で作っておきましょう。

www.visualstudio.com

アカウントの準備ができたら、プロジェクトを作成しておきます。ここではとりあえずプロジェクト名を決めて作成するだけでOKです。

Azure App Serviceを作成

Azureポータルで、アプリケーションを配置するApp Serviceを作成しておきます。
ここで注意が必要なのは、今回の継続的配信機能を使用するためには、Standerd(S1)以上のサービスプランを選択する必要があるという点です。  
Freeプラン(F1)では使用できないので気を付けてください。

継続的配信(プレビュー)の設定

これで下準備は整いました。早速Azureポータルで「継続的配信(プレビュー)」を使ってみましょう。

詳しい手順は、以下のページ(英語)に掲載されていますので、こちらを参考に進めて頂ければ問題ないと思います。

www.visualstudio.com

動かしてみよう

これで必要な設定はすべて完了し、GitHubにPushするだけで最新のアプリケーションがApp Serviceに配置されるはずです。

はずなのですが・・・

ビルドに失敗します

GitHubへのPushをトリガーにVSTSのビルドタスクが動くところまでは問題なかったのですが、そのままではビルド自体が失敗してしまっていました。

VSTSのサイトで、今回のプロジェクトを開いてビルドタスクの設定を確認してみます。
ビルドタスクの設定は「Build&Release」タブで確認できます。

f:id:pierre3:20170402175617p:plain

  • ビルド設定名部分のリンクをクリックして

f:id:pierre3:20170402175631p:plain

  • Editをクリックすると、以下のようなビルドタスクの設定画面が表示されます。

f:id:pierre3:20170402180140p:plain

ビルドタスクの設定を見直す

既定では、ASP.NET Core(PREVIEW)というテンプレートが使用されるようなのですが、そのままではうまくいきませんでした。
更に、各タスクの設定等を変えていろいろ試してもみたのですが、解決しませんでした。

そこで、ASP.NET Core(PREVIEW)のテンプレートを使うのは止めて、以下のページに掲載されている内容で試してみたところビルドが通るようになりました。

www.visualstudio.com

「Get Sources」を除く既定のタスクを全て削除し、「+Add Task」で新しいタスクに置き換えていきます。

以下に設定内容のスクリーンキャプチャを載せておきます。

f:id:pierre3:20170402211628p:plain

f:id:pierre3:20170402211643p:plain

  • Build Solution
    Visual Studio Buildタスクを実行します。

f:id:pierre3:20170402211659p:plain

  • Test Assemblies
    対象のリポジトリにテストプロジェクトが含まれる場合にテストを実行します。

f:id:pierre3:20170402210917p:plain

f:id:pierre3:20170402211715p:plain

  • Archive Files
    ビルド成果物をZIPに圧縮します。

f:id:pierre3:20170402211745p:plain

  • Publish Build Artifacts
    ビルド成果物をサーバーに公開します。

f:id:pierre3:20170402211810p:plain

Build Agentを変更する

もう1つ。ビルド設定画面で「Options」のタブを選択して、右側の「Default agent queue」を”Hosted”から”Hosted VS2017”に変更しておかないとうまくいきませんでした。

f:id:pierre3:20170402184429p:plain

ここまで終えたら、ビルド設定画面の右上にある「Save & Queue」をクリックして、設定したタスクを実行してみます。

f:id:pierre3:20170402215115p:plain

これで、ビルド成果物を作成するところまでは何とか動かすことが確認できたのですが、今度は次のフェーズで失敗してしまいます。

今度はデプロイに失敗する

ビルド成果物の作成が完了したら、Azure App Serviceへ実際に配置を行うReleaseのフェーズに移行するのですが、ここで次のようなエラーが発生して失敗してしまいます。

error: Web Deploy cannot modify the file - ERROR_FILE_IN_USE

どうやら、配置先のDLLがロックされていて上書きできないことが原因のエラーのようです。

※この問題は、GitHubのIssuesでも議論されていました。

Azure Web App Deployment error: Web Deploy cannot modify the file - ERROR_FILE_IN_USE · Issue #1607 · Microsoft/vsts-tasks · GitHub

VSTSで「Release」の設定を見てみると、以下の様になっていました。

f:id:pierre3:20170402224156p:plain

  • 「Deploy Azure App Service to Slot」でStaging用スロットにデプロイ
  • それが成功したら「Manage Azure App Service - Slot Swap」で本番用のスロットにスワップ

という構成になっています。

今回は「Deploy Azure App Service to Slot」でファイルが上書きできずにエラーとなっていました。
これを解決するには、配置先のStagingスロットのサービスを一旦停止してからデプロイを実行する必要がありそうです。

Releaseタスクの設定を変更する

そこで、以下の様に「Deploy Azure App Service to Slot」の前後にサービスの停止、再開を挟むようにしてみたところ無事デプロイされるようになりました。
以下に設定内容を貼っておきます。

  • Manage Azure App Service - Stop App Service

f:id:pierre3:20170402230003p:plain

  • Deploy Azure App Service to Slot

f:id:pierre3:20170402230047p:plain

  • Manage Azure App Service - Start App Service

f:id:pierre3:20170402230059p:plain

Manage Azure App Service - Slot Swap

f:id:pierre3:20170402230111p:plain

まとめ

非常に長くなってしまいましたが、ようやくこれでGitHubリポジトリからAzure App Serviceへの継続的配信の設定はひとまず完了です。

今回試したのはあくまでも(プレビュー)なので、(プレビュー)が取れた暁にはAzureポータルからポチポチと数クリックするだけで済むようになっているかもしれません。(涙目)

CSVファイルの読み書き設定をC#スクリプトで記述するWPFアプリをDesktop App Converterで変換してストアに公開しました

デスクトップアプリをUWPに変換してWindowsストアに公開可能な状態にするDesktop App Converterを試してみたい!
ということで、ブログのネタで作成していたWPFアプリ(CsvEditSharp)をDesktop App Converterに掛けてストアに公開するまでをチャレンジしてみました。

ひとまず、公開まで漕ぎつけることができたので、アプリの宣伝をしておきます。

CsvEditSharp

CsvEditSharpは、CSVファイルの読み書き設定をC#スクリプトで記述するCSVエディタです。
スクリプトでは、C#CSVファイルを扱うためのクラスライブラリCsvHelperAPIを利用して各種設定を記述します。

Windows Storeから無料でダウンロードできます。(Windows10 Anniversary Update 以降のデスクトップPCのみで利用可能)

www.microsoft.com

ソースコードGitHubに公開しています。

github.com

基本操作

設定スクリプトのひな型を生成してCSVファイルを読み込む

初めて扱うCSVファイル等、設定スクリプトが存在していない場合に、CSVファイルの読み込みと同時に設定スクリプトのひな型を生成することができます。

  • ツールバーの「Configuration Script」コンボボックスで "(Auto Genarate)" を選択します

f:id:pierre3:20170106142156p:plain

  • 「Open」ボタンをクリックして読み込むCSVファイルを選択します

  • 以下のダイアログで、自動生成される設定スクリプトの名前、CSVファイルのエンコーディングおよびヘッダレコードの有無を入力して[OK]をクリックします

f:id:pierre3:20161220135026p:plain

次のように、選択したCSVファイルのヘッダ情報を基に設定スクリプトが自動生成されます。

f:id:pierre3:20161220134928p:plain

レコード格納クラスがFieldData という名前で生成されます。

  • クラス内の各プロパティが1つのカラムを表します
  • ヘッダレコードに定義されているカラム名が、そのままプロパティ名となります
  • プロパティのデータ型は全て文字列(string)型となります
  • カラム名にプロパティ名として使用できない文字が含まれる場合、またはヘッダレコードが存在しない場合のプロパティ名にはcolumn_ + カラム番号が割り当てられます

設定スクリプトのひな型をカスタマイズする

設定スクリプトの編集

設定スクリプトを、読み込んだCSVファイルの内容に応じて書き換えます。

Encoding = Encoding.GetEncoding("utf-8");

//Genderの選択肢を定義するenum
enum Gender
{
    Male,
    Female
}

//レコード格納クラスの名前を変更
class Person
{
  //プロパティの型をデータの種類に応じて変更
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
    public Gender Gender { get; set; }public bool Married { get; set; }
    public double PocketMoney { get; set; }
}

//クラスマッピングの設定も、扱うデータの種類に応じたものに変更
RegisterClassMap<Person>(classMap =>
{
    classMap.Map(m => m.Name).Name("Name");
    classMap.Map(m => m.Birthday).Name("Birthday")
        .TypeConverterOption("M/d/yyyy");
    classMap.Map(m => m.Gender).Name("Gender");
    classMap.Map(m => m.Married).Name("Married")
        .TypeConverterOption(true,"Y")
        .TypeConverterOption(false,"N");
        
    var culture = System.Globalization.CultureInfo.GetCultureInfo("en-us");
    classMap.Map(m => m.PocketMoney).Name("PocketMoney")
        .TypeConverterOption("C")
        .TypeConverterOption(NumberStyles.Currency)
        .TypeConverterOption(culture);
});

編集が完了したら[Run]ボタンをクリックして、結果を確認します。

f:id:pierre3:20170106150619p:plain

編集したコードに問題がなければ、編集後の設定でCSVファイルが再読み込みされ、内容がCSVエディタに表示されます f:id:pierre3:20161220135128p:plain

設定スクリプトの保存

編集した設定スクリプトは[Save] (上書き保存)ボタン、[SaveAs...] (名前を付けて保存)ボタンで保存できます。

[SaveAs...]ボタンをクリックすると、以下のダイアログが表示されます。

f:id:pierre3:20170107221931p:plain

  • 「Save as a new file」を選択

  • 「Save into the current directory as "Default.config.csx"」を選択

    • 設定スクリプトを、読み込み中のCSVファイルと同じフォルダに"Default.config.csx"という名前で保存します。
    • 読み込むCSVファイルと同じフォルダに"Default.config.csx"ファイルが存在する場合、常にこのファイルが設定スクリプトとして使用されます。(「Configuration Script」コンボボックスでの選択は無視されます)

設定スクリプトを指定してCSVファイルを読み込む

対応する設定スクリプトが既に存在する場合、「Configuration Script」で対応するスクリプトの名前を選択後、CSVファイルを読み込みます。

f:id:pierre3:20170107225016p:plain

設定スクリプトの管理

[Settings...]ボタンで表示される以下のダイアログで、作成済みの設定スクリプトの名前変更や削除が可能です。  

f:id:pierre3:20170110130931p:plain

値の編集

CSVエディタ(読み込んだCSVファイル名のタブ)内のセルを直接編集することができます。

クラスマッピングでマップしたプロパティのデータ型に応じて入力方法や入力可能な値が変わります。

  • データ型がenumの場合、enumのメンバーを選択肢としたコンボボックスで値を選択します
  • データ型がboolの場合、チェックボックスのON/OFFでTrue/Falseを切り替えます
  • データ型が数値型やDateTime型の場合、セルに入力した文字列が目的の型に変換できない場合にエラーメッセージを表示します。

f:id:pierre3:20170110224957p:plain

AddValidation() メソッドを使用してより詳細な入力検証を設定することも可能です。

編集したCSVデータの保存

ツールバーの[SaveAs]ボタンをクリックして、編集後のCSVデータを別のCSVファイルとして保存することができます。 CSVエディタに表示されている状態がそのまま保存されます。(Queryメソッドでフィルタ・ソートを行った場合も、表示されている内容がそのまま保存されます。)

f:id:pierre3:20170111104107p:plain

設定スクリプトAPI

Encoding プロパティ

Encoding Encoding { get; set; }

対象CSVファイルのエンコーディングを指定します。 省略した場合は Encoding.Defaultが割り当てられます。

Encoding = Encoding.GetEncoding("utf-8");

RegisterClassMap メソッド

void RegisterClassMap<T>();
void RegisterClassMap<T>(Action<CsvClassMap<T>> propertyMapSetter);
void RegisterClassMap<T>(Action<CsvClassMap<T>> propertyMapSetter, RegisterClassMapTarget target);

CsvHelperを利用したクラスマッピングの設定を記述します。

  • 登録するクラスマッピングオブジェクトを引数に取るデリゲートpropertyMapSetter内に設定内容を記述します。
  • 第2引数で、登録したクラスマッピングを使用するタイミングを指定できます。
    • RegisterClassMapTarget.Reader : 読み込み時のみ
    • RegisterClassMapTarget.Writer : 書き込み時のみ
    • RegisterClassMapTarget.Both : 読み書き両用

クラスマッピング設定の記述方法については、CsvHelper 公式ドキュメントのMappingのセクションを参照ください。

//既定のクラスマッピング設定を使用
RegisterClassMap<Person>();

//読み書き両用
RegisterClassMap<Person>(classMap => {
    classMap.Map(m => m.Name);
    classMap.Map(m => m.Birthday);
    classMap.Map(m => m.Gender);
    classMap.Map(m => m.Married)
        .TypeConverterOption(true,"Y")
        .TypeConverterOption(false,"N");
    classMap.Map(m => m.PocketMoney)
        .TypeConverterOption("C")
        .TypeConverterOption(NumberStyles.Currency);
});

//読み込み時のみ
RegisterClassMap<Person>(classMap => {
    classMap.Map(m => m.Name);
    classMap.Map(m => m.Birthday);
    classMap.Map(m => m.Gender);
    classMap.Map(m => m.Married)
        .TypeConverterOption(true,"Y")
        .TypeConverterOption(false,"N");
    classMap.Map(m => m.PocketMoney)
        .TypeConverterOption("C")
        .TypeConverterOption(NumberStyles.Currency);
}, RegisterClassMapTarget.Reader);

SetConfiguration メソッド

void SetConfiguration(Action<CsvConfiguration> configurationSetter);

引数configurationSetterデリゲートに渡されるCsvConfigurationオブジェクトの値を書き換えることで、CSVファイル読み書き時の詳細な設定を記述することができます。

ここで設定可能な項目の詳細は CsvHelper 公式ドキュメントのConfigurationのセクションを参照ください。

SetConfiguration(config =>
{
    config.HasHeaderRecord = false;
    config.AllowComments = true;
    config.Comment = '#';
    config.Delimiter = ';';
    //etc...
});

AddValidation メソッド

void AddValidation<TType, TMember>(Expression<Func<TType, TMember>> memberSelector, Func<TMember, bool> validation, string errorMessage);

値変更時の入力検証機能を追加することができます。

  • memberSelector デリゲート(式木)で対象となるカラムのプロパティを指定します。
  • validation デリゲートで検証を通過する条件を指定します。
  • errorMessage に検証NG時に表示する文字列を指定します。
AddValidation<Person,DateTime>(
    m => m.Birthday , 
    dt => dt <= DateTime.Now.Date,
    "Cannot enter a future date.");

AddValidation<Person, double>(
    m => m.PocketMoney , 
    n => (n > 0) && (n < 10000.0),
    "PocketMoney must be in the range $0 to $10000.");

Query メソッド

void Query<T>(Func<IEnumerable<T>, IEnumerable<T>> query);
void Query<T>(Action<IEnumerable<T>> query);

データのフィルタ、ソート

LINQの拡張メソッドを利用して、表示するデータのフィルタ、ソートを行うことができます。

Query<Person>(source => source
    .Where(m => m.Gender == Gender.Female )
    .Where(m => m.Married )
    .OrderBy(m => m.PocketMoney) );

データの一括更新

ForEach()拡張メソッドを使用して、データを書き換えることも可能です。

Query<Person>( record => record
    .Where( m => m.Gender == Gender.Male )
    .Where( m => m.Married )
    .ForEach( m =>
    {
        m.Name += " *";
        m.PocketMoney = 0;
    })
);

(Queryメソッドは、設定スクリプトのタブではなく、CSVデータ表示タブの下部にあるテキストエディタ内に記述します。下記のようなコードを記述後、[Execute]ボタンをクリックすることで、コードが実行され、表示に反映されます。)

f:id:pierre3:20170110161754p:plain