himanago

C#とかAzureとかMS系の技術メモ中心に書きたいです。最近はLINE関連技術やVUIも多め。

LINE Bot を Azure Functions (C#) で作る際のオウム返しテンプレ

概要

以前 JavaScript + Azure Functions で作る LINE Bot のテンプレについて書きましたが、C# のもなかったじゃん…ということで書いていきます。

himanago.hatenablog.com

いつも横着していたロギングの部分をちゃんと DI で書いてみたので、今後はこれをテンプレとして使っていきたいと思います(よさそうなら VS のテンプレートにもしていけたら)。

プロジェクトテンプレート

Azure Functions v3 (.NET Core) を使用します。

f:id:himanago:20200602022245p:plain

必要な NuGet パッケージ

LineDC.Messaging (v1.2.0)

Messaging APIC# SDK。最新版の機能が含まれているのはこのパッケージです。
もともとあった Line.Messaging は更新が止まり、こちらに移行しているので注意が必要です。

Microsoft.Azure.Functions.Extensions (v1.0.0)

Azure Functions で DI を使うために必要なパッケージです。

このほか、パッケージとしては Microsoft.NET.Sdk.Functions (サンプルでは v3.0.7) が入っている状態です。

作成するファイル

C# のコードで作成するのは 4 つ。

  • LineBotApp.cs
  • Startup.cs
  • WebhookEndpoint.cs
  • Configurations/LineBotSettings.cs

LineBotApp.cs

WebhookApplication を継承した、LINE Bot のメイン処理を司るクラスです。

OnMessageAsync 等のイベントハンドラーをオーバーライドして処理を定義しておくことで、Webhook 時に受け取ったメッセージに応じて呼び出してくれます。
今回は、オウム返し(Echo Bot)なのでメッセージを受け取ったらそのまま返すだけの処理が書いてあります(メッセージの種類で switch)。

もう一つのポイントはコンストラクタです。
DI によって受け取る 3 つの引数を持ち、基底クラスの WebhookApplicationILineMessagingClient とチャネルシークレット(LineBotSettings オブジェクトに含まれる)を渡しながら、自身で使用するロガーを loggerFactory.CreateLogger から生成しています(このクラスは WebhookEndpoint という関数内で使われるので、その関数名を指定しています)。

using LineBotTemplate.Configurations;
using LineDC.Messaging;
using LineDC.Messaging.Webhooks;
using LineDC.Messaging.Webhooks.Events;
using LineDC.Messaging.Webhooks.Messages;
using Microsoft.Azure.WebJobs.Logging;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

namespace LineBotTemplate
{
    public class LineBotApp : WebhookApplication
    {
        private ILogger Logger { get; }

        public LineBotApp(ILineMessagingClient lineMessagingClient, LineBotSettings settings, ILoggerFactory loggerFactory)
            : base(lineMessagingClient, settings.ChannelSecret)
        {
            Logger = loggerFactory.CreateLogger(LogCategories.CreateFunctionUserCategory(nameof(WebhookEndpoint)));
        }

        protected override async Task OnMessageAsync(MessageEvent ev)
        {
            Logger?.LogTrace($"OnMessageAsync => Type: {ev.Source.Type}, Id: {ev.Source.Id}");
            switch (ev.Message)
            {
                case TextEventMessage textMessage:
                    await Client.ReplyMessageAsync(ev.ReplyToken, textMessage.Text);
                    break;
                case MediaEventMessage mediaMessage:
                    await Client.ReplyMessageAsync(ev.ReplyToken, $"contentProvider: {mediaMessage.ContentProvider}");
                    break;
                case FileEventMessage fileMessage:
                    await Client.ReplyMessageAsync(ev.ReplyToken, $"filename: {fileMessage.FileName}");
                    break;
                case LocationEventMessage locationMessage:
                    await Client.ReplyMessageAsync(ev.ReplyToken, $"{locationMessage.Title}({locationMessage.Latitude}, {locationMessage.Longitude})");
                    break;
                case StickerEventMessage stickerMessage:
                    await Client.ReplyMessageAsync(ev.ReplyToken, $"sticker id: {stickerMessage.PackageId}-{stickerMessage.StickerId}");
                    break;
            }
        }
    }
}

Configurations/LineBotSettings.cs

LINE Bot の設定に関するクラス。

namespace LineBotTemplate.Configurations
{
    public class LineBotSettings
    {
        public string ChannelSecret { get; set; }
        public string ChannelAccessToken { get; set; }
    }
}

LineBotSettings:ChannelSecretLineBotSettings:ChannelAccessTokenといったキー名で Function App のアプリケーション設定(Azure での実行時)や local.settings.json (ローカル実行時)に追加しておくと、次に載せる Startup.cs でこのクラスのプロパティの値として使うことができます。

f:id:himanago:20200602023813p:plain

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "LineBotSettings:ChannelAccessToken": "dummy",
    "LineBotSettings:ChannelSecret": "dummy"
  }
}

どちらの値も、LINE Developers で発行・確認できる値を入れておきます。

Startup.cs

DI コンテナへの登録を行う Startup クラスです。

最初に ConfigurationBuilder を使って local.settings.json環境変数(アプリケーション設定)から値を読み取り、LineBotSettings に入れて、そのあとに LineBotSettings のオブジェクトである settingsLineMessagingClientLineBotAppAddSingleton しています。

using LineBotTemplate.Configurations;
using LineDC.Messaging;
using LineDC.Messaging.Webhooks;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(LineBotTemplate.Startup))]
namespace LineBotTemplate
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            var config = new ConfigurationBuilder()
                .AddJsonFile("local.settings.json", true)
                .AddEnvironmentVariables()
                .Build();

            var settings = config.GetSection(nameof(LineBotSettings)).Get<LineBotSettings>();

            builder.Services
                .AddSingleton(settings)
                .AddSingleton<ILineMessagingClient>(_ => LineMessagingClient.Create(settings.ChannelAccessToken))
                .AddSingleton<IWebhookApplication, LineBotApp>();
        }
    }
}

LineBotApp は Webhook 応答で使用するものなのでこのサンプルでも DI で受け取って使用しています。
ILineMessagingClient はサンプルでは直接使用していませんが、何かの関数で Push したりするのに使うために、DI で受け取れるようにしておくと便利です。

ほかに必要なサービス(HttpClient や、CosmosClient)があればここで追加します。
また、DI されるインスタンスの生成方法は、DI コンテナに登録するクラスごとに必ず確認しましょう(よく間違えるので気を付けたい)。

メソッド 内容
AddTransient インジェクションごとに生成
AddScoped リクエストごとに生成
AddSingleton 一度生成されたものを使いまわす

WebhookEndpoint.cs

Azure Functions のプロジェクトテンプレートから作成したときは Function1.cs という名前で作られますが、リネームして中身を書き換えます。

DI するので static を外してコンストラクタで IWebhookApplication を受け取るようにしておきます。Startup で設定した通り、LineBotAppインスタンスが受け取れます。

関数の中の処理としては、受け取ったメッセージをもとに RunAsync を呼ぶだけで OK です(勝手に OnMessageAsync 等は実行してくれる)。

using LineDC.Messaging.Webhooks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Threading.Tasks;

namespace LineBotTemplate
{
    public class WebhookEndpoint
    {
        private readonly IWebhookApplication _app;

        public WebhookEndpoint(IWebhookApplication app)
        {
            _app = app;
        }

        [FunctionName(nameof(WebhookEndpoint))]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
            ILogger log)
        {
            try
            {
                var body = await new StreamReader(req.Body).ReadToEndAsync();
                var xLineSignature = req.Headers["x-line-signature"];

                log.LogTrace($"RequestBody: {body}");
                await _app.RunAsync(xLineSignature, body);
            }
            catch (Exception ex)
            {
                log.LogError(ex.Message);
                log.LogError(ex.StackTrace);
            }

            return new OkResult();
        }
    }
}

まとめ

あらためて、Messaging API の新しい C# SDK 便利です。

Functions でも、DI で使うサービスを整理しておくと拡張もしやすいですね。

サンプルリポジトリ

上記のコードは以下のリポジトリに置いてあります。 github.com