Durable Functions を使ってコードのみでアンケート回答に便利な LINE Bot を簡単につくる

この記事は Azure Advent Calendar 2019 17日目の記事です。

この記事では Azure Functions の拡張機能である Durable Functions を使って作る、アンケート回答をぽんぽんぽんと簡単にできる LINE Bot を紹介します。

## こんな Bot を作ります

アンケート回答 Bot で、回答をクイックリプライで楽々できるものです。
クイックリプライは、ユーザーが Bot への返信を簡単にできるようにするための補助機能です。
（参考）https://developers.line.biz/ja/docs/messaging-api/using-quick-reply/

実行イメージはいかのようなもの。「アンケート開始」と送ると、質問が返ってきます。
<img width="414" alt="IMG_5346.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/c6295dd7-a5a4-d136-c1e8-6752c66a9d8b.jpeg">

とてもなやましい選択肢です。選ぶと次の質問が。

<img width="414" alt="IMG_5347.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/cf58eeb6-3c81-0f85-3759-c9b952ecb57e.jpeg">

そしてさらなる難問が。

<img width="414" alt="IMG_5348.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/5d9f6a6c-8821-c431-04c7-e9a65e8e9541.jpeg">

このようにクイックリプライで回答を繰り返し、最後に確認をし、問題なければ確定、送信という流れのシナリオです。

<img width="414" alt="IMG_5349_p.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/171bfa3c-3553-bfd2-f96b-0a9257777605.jpeg">

これを Durable Functions で簡単に作ろうというのが今回の記事の内容です。

## Durable Functions って？

サーバーレスなコード実行環境である Azure Functions に、ステートフルな状態管理・監視機構を追加できる公式の拡張機能です。
※ Durable Functions でどういうことができるか？は下記記事参照
[Azure Functions の 超イケてる Durable Functions を使ってみる](https://qiita.com/TsuyoshiUshio@github/items/3e8acb1b0388b6045fdb)

通常、LINE Bot をサーバーレス環境で使うと、オウム返しなどのシンプルな一問一答（1往復で完結するスタイル）の Bot を作るのは簡単ですが、複数回の対話の往復をサーバー側が覚えているような対話フローを作るには何らかのストレージや DB を使う必要があり、けっこうめんどうです。

今回テーマとなるアンケート回答も、こういうシナリオが多いと思います。
複数の質問を回答し、最後に確認をしてからまとめて送信・DB 保存、という流れなので、アンケート開始から最終的に回答を送信するまで、それまでの回答を覚えていることが必要です。

確定前の回答途中のものを DB に保存するのはちょっと…なので、ここで Durable Functions を使って、コードのみ・いっさい DB 処理を書かずに、ステートフルに回答内容を保持する処理を実現してしまいましょう。

## つくりかた

### 環境の準備
今回は利用者も多い VS Code でやってみます。

まずは以下をインストールしておきます。

- [Node.js:](https://nodejs.org/ja/)（LTS 版をいれておく）
- [.NET Core SDK 3.1](https://dotnet.microsoft.com/download/dotnet-core/3.1)
- [VS Code](https://code.visualstudio.com/)


VS Code の拡張機能として以下を入れておきます。

- Azure Functions
- C#

また、コマンドで以下を実行し、Azure Functions Core Tools を入れておきます（今回は GA したての v3 を使っていきます）。

```
npm install -g azure-functions-core-tools@3
```


### プロジェクト作成

Azure Functions の拡張機能（左の Azure アイコンをクリック）から、Azure Functions 用のプロジェクトを作成します。

![010_create_new_proj.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/31cb6fb0-5df5-7246-96bd-15b7767cac2c.png)

ウィザードに従ってぽちぽちと進めていきます。

プロジェクト名は `durable-enq-bot`、言語 C#、

![030_create_new_proj_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/d691e45b-f98e-7f2f-41a4-52a87c719634.png)

テンプレートは「DurableFunctionsOrchestration」を選択しましょう。

![040_create_new_proj_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/3c0c927b-5473-085d-45ee-3f26d3123ac1.png)

また、関数名に `DurableEnqFunctions`、名前空間に `Sample.LineBot` と入力します。

最後にストレージをどうするか聞かれますが、「Use local emulator」を選んでおけば OK です（本記事ではローカル実行は行わないのでどれでもいいです）。

### コードを書く（まずは雛形）

#### csproj ファイル

テンプレートが古かったので、2019/12/17 時点の最新版である .NET Core 3.1 / Azure Functions v3 にし、NuGet パッケージも追加・更新します。

- Microsoft.Azure.Functions.Extensions: 1.0.0 → DI の仕組みを使うために必要
- Microsoft.Azure.WebJobs.Extensions.DurableTask: 2.0.0 → Durable Functions 2.0
- Microsoft.NET.Sdk.Functions: 3.0.2 → Azure Functions v3
- LineDC.Messaging: 1.0.0 → LINE Messaging API 用 SDK の最新版

```xml:durable-enq-bot.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <AzureFunctionsVersion>v3</AzureFunctionsVersion>
    <RootNamespace>Sample.LineBot</RootNamespace>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.0.0" />
    <PackageReference Include="Microsoft.Azure.WebJobs.Extensions.DurableTask" Version="2.0.0" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.2" />
    <PackageReference Include="LineDC.Messaging" Version="1.0.0" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>
```

#### Bot 本体の雛形を作成

Bot 本体の雛形を作ります。LINE Messaging API SDK に `IWebhookApplication` という便利なインターフェースが用意されているので、これをベースにします。

まずは Durable Functions の機能と Azure Functions の `log` を使うためにインターフェースを拡張します。

```cs:IDurableWebhookApplication.cs
using LineDC.Messaging.Webhooks;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Extensions.Logging;

namespace Sample.LineBot
{
    public interface IDurableWebhookApplication : IWebhookApplication
    {
        ILogger Logger { get; set; }
        IDurableOrchestrationClient DurableClient { get; set; } 
    }
}
```

このインターフェースと `WebhookApplication` を使って、Bot 本体となる `EnqBotApp` クラスを作ります。

```cs:EnqBotApp.cs
using LineDC.Messaging;
using LineDC.Messaging.Webhooks;
using LineDC.Messaging.Webhooks.Events;
using LineDC.Messaging.Webhooks.Messages;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

namespace Sample.LineBot
{
    public class EnqBotApp : WebhookApplication, IDurableWebhookApplication
    {
        public ILogger Logger { get; set; }
        public IDurableOrchestrationClient DurableClient { get; set; }

        public EnqBotApp(ILineMessagingClient client, string channelSecret)
            : base(client, channelSecret)
        {
        }

        protected override async Task OnMessageAsync(MessageEvent ev)
        {
        }

        protected override async Task OnPostbackAsync(PostbackEvent ev)
        {
        }
    }
}
```

このクラスは On○○ メソッドをオーバーライドすることで、LINE 側からの Webhook イベント発生時、その種類に合わせたメソッドが自動で実行されます。
そのため開発者はイベントごとの処理を書くだけで済むのでとても便利です。

#### Startup クラスを追加

`EnqBotApp` を Azure Functions で使えるよう、SDK の `LineMessagingClient` とともに DI します。

```cs
using System;
using LineDC.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(Sample.LineBot.Startup))]
namespace Sample.LineBot
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services
                .AddSingleton<ILineMessagingClient>(_ => LineMessagingClient.Create(Environment.GetEnvironmentVariable("ChannelAccessToken")))
                .AddTransient<EnqBotApp>(s => new EnqBotApp(s.GetService<ILineMessagingClient>(), Environment.GetEnvironmentVariable("ChannelSecret")));
        }
    }
}
```
※ 追記 ※
@okazuki さんに指摘いただいたので、DI スコープを修正しました（`EnqBotApp` がシングルトンだと後述のロガー/starterの追加で複数ユーザー同時実行でおかしくなる）。
また、`s.GetService<T>()` という便利なメソッドも教えていただきました。ありがとうございます。

LINE Bot のバックエンドのために必要なチャネルアクセストークンとチャネルシークレットは環境変数から取得するようにします（LINE の Messaging API 側の設定時に取得できる値）。

ちなみに DI のやりかたが違うと例外が発生する場合があります。
（参考）https://himanago.hatenablog.com/entry/2019/12/17/122643

#### 関数

続いて関数です。Durable Functions のスターター（HTTP トリガー）、オーケストレーター、アクティビティのそろったクラスが作成されていますが、こちらもテンプレートから作られたものが古い v1 形式だったので、[ドキュメント](https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-versions)も参考にしながら直していきます。

また、DI でコンストラクタインジェクションするため、静的クラスをやめます。クラスとメソッドから `static` をすべて外せば OK です。

コンストラクタで `EnqBotApp` を受け取り `Logger` と `DurableOrchestrationClient` を HTTP トリガーの関数で渡してあげます。


```cs
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;

namespace Sample.LineBot
{
    public class DurableEnqFunctions
    {
        private EnqBotApp App { get; }

        public DurableEnqFunctions(EnqBotApp app)
        {
            App = app;
        }

        [FunctionName("DurableEnqFunctions")]
        public async Task<List<string>> RunOrchestrator(
            [OrchestrationTrigger] IDurableOrchestrationContext context)
        {
            var outputs = new List<string>();

            // Replace "hello" with the name of your Durable Activity Function.
            outputs.Add(await context.CallActivityAsync<string>("DurableEnqFunctions_Hello", "Tokyo"));
            outputs.Add(await context.CallActivityAsync<string>("DurableEnqFunctions_Hello", "Seattle"));
            outputs.Add(await context.CallActivityAsync<string>("DurableEnqFunctions_Hello", "London"));

            // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
            return outputs;
        }

        [FunctionName("DurableEnqFunctions_Hello")]
        public string SayHello([ActivityTrigger] string name, ILogger log)
        {
            log.LogInformation($"Saying hello to {name}.");
            return $"Hello {name}!";
        }

        [FunctionName("DurableEnqFunctions_HttpStart")]
        public async Task<HttpResponseMessage> HttpStart(
            [HttpTrigger(AuthorizationLevel.Function, "post")]HttpRequestMessage req,
            [DurableClient]IDurableOrchestrationClient starter,
            ILogger log)
        {
            // EnqBotApp にロガーとスターターをわたす
            App.Logger = log;
            App.DurableClient = starter;

            // Bot の応対処理を呼び出し
            await App.RunAsync(
                req.Headers.GetValues("x-line-signature").First(), await req.Content.ReadAsStringAsync());

            return req.CreateResponse(HttpStatusCode.OK);
        }
    }
}
```

### いったんビルド

ここまで来たら、SDK 類をきちんと最新化して、コードの移行もできていることを確認するためいったんリストア・ビルドをします。

VS Code のターミナルから、以下のコマンドを実行します。

```powershell
dotnet restore
```

```powershell
dotnet build
```

ビルドが通ることを確認し、もしエラーが残る場合も VS Code を再起動すれば消えるはずですが、消えなければコード等を見直します。
ここまでで準備完了なので、実際の処理を作っていきます。

### アンケート回答のための関数オーケストレーションの実装

#### アンケート

では、ここからアンケート回答のコードを書いていきます。
実際に使う場合は DB から取得する形になると思いますが、今回は適当につくっておきます。

```cs
// アンケート（選択式3問＋自由記述1問）
private List<(string question, string[] quickReply)> enq = new List<(string question, string[] quickReply)>
{
    ("Azure は好きですか？", new [] { "はい", "Yes" }),
    ("Azure Functions は好きですか？", new [] { "はい", "もちろん", "大好きです" }),
    ("Web Apps は？", new [] { "好きです", "大好きです" }),
    ("Azure で好きなサービスは？", null)
};
```

回答に選択肢を用意していますが、これは LINE Bot のクイックリプライ用です。

#### オーケストレーション

今回のアンケート回答は、ユーザーの「アンケート開始」を合図に Bot が上記のアンケート質問を返信・その回答に対してさらに次の質問を返信…と、クイックリプライも使ってぽんぽんぽんとアンケートに回答できる仕組みです。
すべての質問に回答しおわったら、そこまでの回答をいったんまとめて、ユーザーに確認します。確認後、問題なければ「送信」して DB などに保存する、という流れです（今回はサンプルのため最後の DB 保存処理が作りません）。

これは「アンケート開始」のメッセージを受け取ったバックエンドが、オーケストレーターを起動することで実現します。

オーケストレーターの中で Durable Functions の一機能である「外部イベントの待機」を行い、`"answer"` という名前のイベントが起きるまで処理を止めて待つことができます。

`"answer"` というイベントはユーザーの回答メッセージ送信時にこのイベントを投げるよう実装します。

このイベントを受け取ったら、回答をバックエンドが入手できるのですが、バックエンド側で保持する必要があります。そこでオーケストレーターは、自分自身を（再帰的に）呼び出し、入手した回答を渡します。これを繰り返すことで、複数の質問と回答の入手を繰り返していくことができます。

- 外部イベントの待機は `WaitForExternalEvent`
- イベントを投げるのは `RaiseEventAsync`
- オーケストレーターの再帰実行は `ContinueAsNew`

です。これらを踏まえて実装すると、以下のようになります。細かい制御をいろいろと行っていますが、コメントを細かくつけたので処理を追う参考にしてみてください。

```cs:DurableEnqFunctions.cs(完成版)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;

namespace Sample.LineBot
{
    public class DurableEnqFunctions
    {
        private EnqBotApp App { get; }
        public DurableEnqFunctions(EnqBotApp app)
        {
            App = app;
        }

        [FunctionName(nameof(EnqAnswerOrchestrator))]
        public async Task EnqAnswerOrchestrator(
            [OrchestrationTrigger] IDurableOrchestrationContext context,
            ILogger log)
        {
            // アンケート回答をまとめるリスト（再帰呼び出し時は引数から受け取る）
            var list = context.GetInput<List<string>>() ?? new List<string>();

            // 「answer」という名前のイベントが起きるまで待機
            // 発生後、その結果を value に受け取る
            var value = await context.WaitForExternalEvent<(int index, string message, string replyToken)>("answer");
            log.LogInformation($"EnqAnswerOrchestrator - index: {value.index}");

            // リストに回答を追加
            list.Add(value.message);

            // インデックスが -1 なら終了とみなす
            if (value.index == -1)
            {
                // 完成したリストをリプライトークンとともに確認返信アクティビティに渡す
                await context.CallActivityAsync(nameof(SendSummaryActivity), (value.replyToken, list));
            }
            else
            {
                // 回答インデックスを更新
                context.SetCustomStatus(value.index);

                // 質問返信アクティビティ呼び出し
                // あえてオーケストレーターの最後で遠回しに返信処理を呼ぶのは、
                // インデックス更新前に返信されて同じ質問が返るのを防ぐため。
                // 返信などもアクティビティに任せないとおかしくなる（オーケストレーターは冪等性を維持）
                await context.CallActivityAsync(nameof(SendQuestionActivity), (value.replyToken, value.index + 1));

                // オーケストレーターを再帰的に呼び出す
                context.ContinueAsNew(list);
            }
        }

        [FunctionName(nameof(SendQuestionActivity))]
        public async Task SendQuestionActivity(
            [ActivityTrigger] IDurableActivityContext context)
        {
            var input = context.GetInput<(string replyToken, int index)>();

            await App.ReplyNextQuestionAsync(input.replyToken, input.index);
        }

        [FunctionName(nameof(SendSummaryActivity))]
        public async Task SendSummaryActivity(
            [ActivityTrigger] IDurableActivityContext context)
        {
            var input = context.GetInput<(string replyToken, List<string> answers)>();

            // アンケート回答の確認メッセージを返信
            await App.ReplySummaryAsync(input.replyToken, input.answers);
        }

        [FunctionName(nameof(HttpStart))]
        public async Task<HttpResponseMessage> HttpStart(
            [HttpTrigger(AuthorizationLevel.Function, "post")]HttpRequestMessage req,
            [DurableClient]IDurableOrchestrationClient starter,
            ILogger log)
        {
            // EnqBotApp にロガーとスターターをわたす
            App.Logger = log;
            App.DurableClient = starter;

            await App.RunAsync(
                req.Headers.GetValues("x-line-signature").First(), await req.Content.ReadAsStringAsync());

            return req.CreateResponse(HttpStatusCode.OK);
        }
    }
}
```

```cs:EnqBotApp.cs(完成版)
using LineDC.Messaging;
using LineDC.Messaging.Messages;
using LineDC.Messaging.Messages.Actions;
using LineDC.Messaging.Messages.Flex;
using LineDC.Messaging.Webhooks;
using LineDC.Messaging.Webhooks.Events;
using LineDC.Messaging.Webhooks.Messages;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Sample.LineBot
{
    public class EnqBotApp : WebhookApplication, IDurableWebhookApplication
    {
        public ILogger Logger { get; set; }
        public IDurableOrchestrationClient DurableClient { get; set; }

        public EnqBotApp(ILineMessagingClient client, string channelSecret)
            : base(client, channelSecret)
        {
        }

        // アンケート（選択式3問＋自由記述1問）
        private List<(string question, string[] quickReply)> enq = new List<(string question, string[] quickReply)>
        {
            ("Azure は好きですか？", new [] { "はい", "Yes" }),
            ("Azure Functions は好きですか？", new [] { "はい", "もちろん", "大好きです" }),
            ("Web Apps は？", new [] { "好きです", "大好きです" }),
            ("Azure で好きなサービスは？", null)
        };

        protected override async Task OnMessageAsync(MessageEvent ev)
        {
            if (ev.Message is TextEventMessage textMessage)
            {
                if (textMessage.Text == "アンケート開始")
                {
                    // 履歴削除
                    await DurableClient.PurgeInstanceHistoryAsync(ev.Source.UserId);

                    // オーケストレーター開始
                    await DurableClient.StartNewAsync(nameof(DurableEnqFunctions.EnqAnswerOrchestrator), ev.Source.UserId);

                    // 最初の質問
                    await ReplyNextQuestionAsync(ev.ReplyToken, 0);
                }
                else
                {
                    // オーケストレーターのステータスを取得
                    var status = await DurableClient.GetStatusAsync(ev.Source.UserId);

                    // カスタムステータスに保存されている回答済み質問インデックスをもとに現在のインデックスを取得
                    int index = int.TryParse(status?.CustomStatus?.ToString(), out var before) ? before + 1 : 0;
                    Logger.LogInformation($"OnMessageAsync - index: {index}");

                    if (enq.Count() == index + 1)
                    {
                        // 回答終了処理
                        // Durable Functionsの外部イベントとして送信メッセージを投げる
                        // 終了の合図「-1」とリプライトークンをセットで送るのがポイント
                        await DurableClient.RaiseEventAsync(ev.Source.UserId, "answer", (-1, textMessage.Text, ev.ReplyToken));
                        return;
                    }

                    // オーケストレーター起動中の場合
                    if (status?.RuntimeStatus == OrchestrationRuntimeStatus.ContinuedAsNew ||
                        status?.RuntimeStatus == OrchestrationRuntimeStatus.Pending ||
                        status?.RuntimeStatus == OrchestrationRuntimeStatus.Running)
                    {
                        // Durable Functionsの外部イベントとしてインデックスと回答内容、リプライトークンをタプルにまとめて投げる
                        await DurableClient.RaiseEventAsync(
                            ev.Source.UserId, "answer", (index, textMessage.Text, ev.ReplyToken));
                    }
                    else
                    {
                        await Client.ReplyMessageAsync(ev.ReplyToken, "「アンケート開始」と送ってね");
                        return;
                    }
                }
            }
            else
            {
                await Client.ReplyMessageAsync(ev.ReplyToken, "「アンケート開始」と送ってね");
            }
        }

        protected override async Task OnPostbackAsync(PostbackEvent ev)
        {
            switch (ev.Postback.Data)
            {
                // 内容確認後の最終送信
                case "send":
                    // 本来はここで DB 保存処理などを行う
                    await Client.ReplyMessageAsync(
                        ev.ReplyToken, "回答ありがとうございました。");
                    // 履歴削除
                    await DurableClient.PurgeInstanceHistoryAsync(ev.Source.UserId);
                    break;

                // キャンセル
                case "cancel":
                    // やり直し
                    await Client.ReplyMessageAsync(
                        ev.ReplyToken, "回答をキャンセルしました。もう一度回答する場合は「アンケート開始」と送ってください。");
                    // 履歴削除
                    await DurableClient.PurgeInstanceHistoryAsync(ev.Source.UserId);
                    break;
            }
        }

        public async Task ReplyNextQuestionAsync(string replyToken, int index)
        {
            // 次の質問
            var next = enq[index];

            await Client.ReplyMessageAsync(replyToken, new List<ISendMessage>
            {
                // クイックリプライがあれば質問とセットで返信
                next.quickReply != null
                    ? new TextMessage(next.question, new QuickReply(next.quickReply.Select(
                        quick => new QuickReplyButtonObject(new MessageTemplateAction(quick, quick))).ToList()))
                    : new TextMessage(next.question)
            });
        }

        public async Task ReplySummaryAsync(string replyToken, List<string> answers)
        {
            // 内容確認を Flex Message で
            await Client.ReplyMessageAsync(replyToken,                
                new List<ISendMessage>
                {
                    FlexMessage.CreateBubbleMessage("確認").SetBubbleContainer(
                        new BubbleContainer()
                            .SetHeader(BoxLayout.Horizontal)
                                .AddHeaderContents(new TextComponent
                                    {
                                        Text = "以下の内容でよろしいですか？",
                                        Align = Align.Center,
                                        Weight = Weight.Bold
                                    })
                            .SetBody(new BoxComponent
                            {
                                Layout = BoxLayout.Vertical,
                                // 質問と回答をまとめて処理（こういうときは Zip メソッドが便利！）
                                // 2つのリストから同じインデックスの項目ごとにペアを作ってくれる
                                Contents = enq.Zip(answers, (enq, answer) => (enq.question, answer)).Select(p => new BoxComponent
                                {                                    
                                    Layout = BoxLayout.Vertical,
                                    Contents = new IFlexComponent[]
                                    {
                                        new TextComponent
                                        {
                                            Text = p.question,
                                            Size = ComponentSize.Xs,
                                            Align = Align.Start,
                                            Weight = Weight.Bold
                                        },
                                        new TextComponent
                                        {
                                            Text = p.answer,
                                            Align = Align.Start
                                        }
                                    }
                                }).ToArray()
                            })
                            .SetFooter(new BoxComponent
                            {
                                Layout = BoxLayout.Horizontal,
                                Contents = new IFlexComponent[]
                                {
                                    new ButtonComponent
                                    {
                                        Action = new PostbackTemplateAction("送信する", "send")
                                    },
                                    new ButtonComponent
                                    {
                                        Action = new PostbackTemplateAction("やり直す", "cancel")
                                    }
                                }
                            }))
                });
        }
    }
}
```

多少長いですが、オーケストレーターを中心としたステート管理のロジック自体はとてもシンプルなコードで実現できていることがわかります（慣れると、普通のプログラミングで関数を呼びあうように Azure Functions 同士を連携できるので、1つ上の次元でプログラミングしてる感じでとても楽しい）。これが Durable Functions の強みです。


### Azure にデプロイ

コードが完成したら Azure にデプロイします。VS Code の拡張から簡単にできます。

「Sign in Azure」から Azure のアカウントでサインインし、デプロイボタンをクリックします。

![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/23ceab6f-a909-3461-fd47-f50aeab3795e.png)

![002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/c2d6ebe9-bd33-7cdd-11d6-c2b0a6e4fd7a.png)

新規リソースの作成（「Create new Function App in Azure...」）を選び、リソース名など必要事項を入力しデプロイを開始します。


![003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/c51c0721-2ee0-c7b3-5886-f16ce73c936c.png)

![004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/e73039a7-8a17-a64e-6e04-74f3bd7b37b0.png)

VS Code の右下に以下のようなメッセージが表示されたらデプロイ完了です。

![005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/16ab16e6-eeaa-805b-ce45-0bad7147f27b.png)




### LINE Messaging API の設定

続いて、LINE Bot の準備をします。Bot には LINE 公式アカウントが必要なので、[LINE Developers](https://developers.line.biz/ja/) で作成します。

詳細は省略しますが、自分の LINE アカウントでログインし、「Messaging API」のチャネルを作っていきます。

![line_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/1f075f45-87da-69d6-b029-e4c28d53424d.png)

必要事項を適当に入力して Messaging API チャネルを作成すると、

- チャネルシークレットの取得
- チャネルアクセストークンの発行・取得
- 応答メッセージ・あいさつメッセージの無効化
- Webhook 設定

ができるようになります（チャネルシークレットとチャネルアクセストークンはこの後使います）。

応答メッセージ（Bot でない自動返信機能）は必ず無効にしてください。
これを無効にする際、「LINE Official Account Manager」という別のサイトに飛びますが、Webhook 設定も遷移先でやってしまうのがおすすめです（LINE Developers だと設定が反映されないことがあるようです）。

![line_002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/b60c03d0-7f7f-3f78-eee3-9748acf6d920.png)


Webhook URL には、さきほどデプロイした Function App の `HttpStart` 関数の URL を設定します。Azure ポータルからさきほどの Function App を開き、`HttpStart` の関数 URLをコピーしてきます。

![az_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/27280d83-cbae-d10c-7045-457a97d5f6ca.png)



これを設定したら完了です。LINE Developers の QR コードから、自分の LINE に友だち追加しておきましょう。


### Azure Functions の環境変数設定

続いて、Azure 側に LINE のキー情報を追加します。Messaging API の、チャンネルアクセストークンとチャンネルシークレットを登録します。

Function App を選択し、「プラットフォーム機能」「構成」をクリックします。

![az_002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/dbe6594a-d1fb-49bc-0408-61196af45d6a.png)

「＋新しいアプリケーション設定」から変数を追加できるので、

![az_003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/d3288106-4940-864c-4c1b-5c5692205d29.png)

コード上の環境変数名に合わせ、`ChannelSecret`, `ChannelAccessToken` のキー名でそれぞれ設定します。

これで LINE と Azure がつながるようになり、LINE Bot が完成しました。

### 実行
クイックリプライで、ぽんぽんぽんと回答。

<img width="414" alt="IMG_5349.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/0ff53258-f1db-10cd-6bf2-2ce5de79f591.jpeg">

できました！

## Durable Functions のステート管理を確認
Durable Functions は、ステート管理のために Azure Table Storage にデータを入れ、それを見ることでオーケストレーションを実現しています。

Azure Storage Explorer でみてみると、以下のようにコードで利用しているインデックス情報や、回答途中の内容などが記録されていることがわかります。

![storage_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194664/d94c3cd5-3825-28ba-0691-151a2a4bc9d5.png)

デバッグ時など、ここを見ながら開発していきます（場合によっては手動で削除したりします）。


## 今回の Bot を実戦で使う場合は
アンケート内容や、最終回答結果は DB に格納するようにしたいです。

なので、

- 最初のアンケートの質問リストを DB から取得するようにする
- 最終の回答結果は DB に保存する
  
とすればよいでしょう。

また、今回のサンプルは本題と関係ない部分が甘々なので（たとえば最後の送信ボタンが何度も押せる、など）、そのあたりの作りこみも必須です。

## ソースコード
以下に一式置いておきました。
https://github.com/himanago/durable-enq-bot

## まとめ
Durable Functions を使うと、コードのみで１つ上の高みへ行くことができます。

オーケストレーションの制御面など注意すべき点は多く、使う場面は選びますが、LINE Bot のような比較的試しやすいプラットフォームを組み合わせると、おもしろいものがどんどん作れると思います。

こういったサンプル作成を通して Durable Functions に慣れておくと、いざ適用できる場面に遭遇したときに大きな武器になると思います。ぜひ、触ってみてほしいなと思います。