概要
以前 JavaScript + Azure Functions で作る LINE Bot のテンプレについて書きましたが、C# のもなかったじゃん…ということで書いていきます。
いつも横着していたロギングの部分をちゃんと DI で書いてみたので、今後はこれをテンプレとして使っていきたいと思います(よさそうなら VS のテンプレートにもしていけたら)。
プロジェクトテンプレート
Azure Functions v3 (.NET Core) を使用します。
必要な NuGet パッケージ
LineDC.Messaging (v1.2.0)
Messaging API の C# 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 つの引数を持ち、基底クラスの WebhookApplication
に ILineMessagingClient
とチャネルシークレット(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:ChannelSecret
、LineBotSettings:ChannelAccessToken
といったキー名で Function App のアプリケーション設定(Azure での実行時)や local.settings.json
(ローカル実行時)に追加しておくと、次に載せる Startup.cs
でこのクラスのプロパティの値として使うことができます。
{ "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
のオブジェクトである settings
と LineMessagingClient
、LineBotApp
を AddSingleton
しています。
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