かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Bot Framework SDK v4 を使うときの 2019/02/12 時点で最適だと思う ConfigureServices メソッドの書き方

なんというか Bot Framework SDK v4 は v3 と違って細かくカスタマイズ可能な部品が提供されてて、それを組み合わせて使うという感じになってるので、より挙動のカスタマイズがしやすくなっていたりします。

例えば LUIS や QnA Maker との連携機能の提供のされかたも v3 では LUIS や QnA Maker と連携するためのダイアログの基本クラスが提供されていて、利用者は基本的には Attribute を付けたメソッドを適切に定義していれば、よしなにライブラリ側が呼び出してくれるといった形になってます。

v4 は API 叩くためのクラス用意してやるから、それ使って結果の処理はお好きにどうぞといった感じです。個人的には好きなアプローチ。 例えば v3 の頃は細かい挙動を変えたいときに基本クラスのソースコードを見て、該当の処理をしているメソッドをオーバーライドして親クラスの処理を上書きしたり、場合によっては基本クラスでは意図されていなかったハードコードされてる挙動を変えたい場合は泣く泣くソースコードを丸コピして該当部分を書き換えたものを自分のプロジェクトに含めたりといった対応が必要でした。(レアケースだとは思いたいですが)

v4 は API 呼ぶだけの機能がある感じなので、あとは好きにしろ。お前の自由だって感じなのでそういう心配がいりません。

ということで、それらの部品群のおぜん立てをするのが ASP.NET Core を使う場合には Startup.cs クラスの ConfigureServices メソッドになるのですが、今日時点の Bot Framework の Visual Studio 向けのプロジェクトテンプレートで新規作成すると非推奨なプロパティを使ってたりしてて個人的に気持ち悪かったので、今のところこう書くのがいいんじゃない?っていうのを書いておこうと思います。

というわけでコード

前置きが長くなったのでコードをさくっと。因みに各 Bot Framework SDK 関連の NuGet は v4.2.2 使ってます。

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Configuration;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Linq;

namespace HelloWorldBot
{
    public class Startup
    {
        private readonly bool _isProduction;
        private ILoggerFactory _loggerFactory;

        public Startup(IHostingEnvironment env)
        {
            _isProduction = env.IsProduction();

            Configuration = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables()
                .Build();
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddBot<HelloWorldBot>(options =>
            {
                var secretKey = Configuration.GetSection("botFileSecret")?.Value;
                var botFilePath = Configuration.GetSection("botFilePath")?.Value ?? @".\xxx.bot"; // 定義がない場合はデフォルトのボットファイルで

                // 定義ファイルを読み込んで IServiceCollection に追加しとく
                var botConfig = BotConfiguration.Load(botFilePath, secretKey);
                services.AddSingleton(botConfig);

                // 環境に応じたエンドポイントの定義を探す
                var service = botConfig.Services.Where(s => s.Type == "endpoint" && s.Name == (_isProduction ? "production" : "development")).FirstOrDefault();
                if (!(service is EndpointService endpointService))
                {
                    throw new InvalidOperationException($"The .bot file does not contain a development endpoint.");
                }

                // 認証機能とエラー時の処理を追加
                options.CredentialProvider = new SimpleCredentialProvider(endpointService.AppId, endpointService.AppPassword);
                var logger = _loggerFactory.CreateLogger<HelloWorldBot>();
                options.OnTurnError = async (context, exception) =>
                {
                    logger.LogError($"Exception caught : {exception}");
                    await context.SendActivityAsync("Sorry, it looks like something went wrong.");
                };
            });

            // アクセサーの登録
            services.AddSingleton(sp =>
            {
                // ここら辺で State 関連は定義しておく。(使う前で定義されてればいい)
                // テンプレートだと非推奨な BotFrameworkOptions クラスの State プロパティで管理してる
                var dataStore = new MemoryStorage(); // 本番は Blog か CosmosDB で
                var userState = new UserState(dataStore);
                var conversationState = new ConversationState(dataStore);

                return new HelloWorldBotAccessors(conversationState, userState)
                {
                    MyUserState = userState.CreateProperty<MyUserState>($"{nameof(HelloWorldBotAccessors)}.{nameof(HelloWorldBotAccessors.UserState)}"),
                    MyConversationState = conversationState.CreateProperty<MyConversationState>($"{nameof(HelloWorldBotAccessors)}.{nameof(HelloWorldBotAccessors.ConversationState)}"),
                };
            });

            // その他サービスもこの下らへんで登録しておく
            services.AddSingleton<BotServices>();
        }

        public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
        {
            _loggerFactory = loggerFactory;
            app.UseDefaultFiles()
                .UseStaticFiles()
                .UseBotFramework();
        }
    }
}

因みに Bot Framework SDK のサンプルを見ると、よく BotServices っていうクラス名見るけど、これは単なる自作クラスで以下のような感じで定義されてます。

using Microsoft.Bot.Builder.AI.Luis;
using Microsoft.Bot.Builder.AI.QnA;
using Microsoft.Bot.Configuration;
using System.Collections.Generic;

namespace HelloWorldBot
{
    public class BotServices
    {
        public Dictionary<string, QnAMaker> QnAServices { get; } = new Dictionary<string, QnAMaker>();
        public Dictionary<string, LuisRecognizer> LuisServices { get; } = new Dictionary<string, LuisRecognizer>();
        public BotServices(BotConfiguration botConfiguration)
        {
            foreach (var service in botConfiguration.Services)
            {
                switch (service.Type)
                {
                    case ServiceTypes.QnA:
                        {
                            var qna = (QnAMakerService)service;
                            // ここらへんで QnAMaker クラスの初期化と登録
                            break;
                        }
                    case ServiceTypes.Luis:
                        {
                            var luis = (LuisService)service;
                            // ここらへんで LuisRecognizer の初期化と登録
                            break;
                        }
                }
            }
        }
    }
}

LUIS や QnA Maker と連携したかったら、この BotServices クラスを DI して使ってねって感じです。サンプルでは Dictionary 型で定義されてることが多いけど、かちっとやるなら個別プロパティかなぁ?どっちがいいんだろう。個人的な感覚では Dictionary よりもきちんと個々のプロパティ定義する方が好みだけど。

まとめ

そろそろテンプレート安定してほしい。