Quantcast
Channel: かずきのBlog@hatena
Viewing all articles
Browse latest Browse all 1387

Bot Frameworkでテスタブルな感じに作りたいのでDIしてみた

$
0
0

Bot Frameworkは実はAutofacを使ってるみたいですね。

github.com

ただ、これを使う方法のドキュメントが見つけられない…。サンプルの中には、これを使って作られてるものがあったりもします。

github.com

でも、微妙にInternalsな名前空間使ってたりして、もんにょりするのですがとりあえずDI出来る感じにする手順をやってみたいと思います。因みに、2017/07/05現在の情報なので割とさくっとやり方かわるかもしれないので注意です。

作ってみよう

Bot Frameworkのプロジェクトを作ります。

Update NuGet packages

作ったらAutofacとSystem.IdentityModel.Tokens.Jwtの2つ以外を最新にします。Autofacは3系と4系で.NET Std対応してたりするので怖くて上げてません。Jwtのほうは何かあげれませんでした。

とりあえずサービス作ってみる

今回はDIすることが目的なので、さくっと固定メッセージを返すだけの以下のようなインターフェースとクラスをでっちあげました。

namespace DITestBotApp.Services
{
    publicinterface IGreetService
    {
        string GetMessage();
    }

    publicclass GreetService : IGreetService
    {
        publicstring GetMessage()
        {
            return"これはDIしたサービスから返されたメッセージです。";
        }
    }
}

DIコンテナのセットアップはConversationクラスのUpdateContainerメソッドで行います。Global.asax.csあたりでやるっぽいですね。先ほど作成したGreetServiceを登録しています。ContainerBuilderが渡ってくるのでよしなにするという感じです。ここらへんはAutofacのドキュメント参照って感じですね。

using Autofac;
using DITestBotApp.Dialogs;
using DITestBotApp.Services;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Internals.Fibers;
using System.Web.Http;

namespace DITestBotApp
{
    publicclass WebApiApplication : System.Web.HttpApplication
    {
        protectedvoid Application_Start()
        {
            GlobalConfiguration.Configure(WebApiConfig.Register);

            Conversation.UpdateContainer(builder =>
            {
                builder.RegisterType<RootDialog>()
                    .As<IDialog<object>>()
                    .InstancePerLifetimeScope();

                builder.RegisterType<GreetService>()
                    .Keyed<IGreetService>(FiberModule.Key_DoNotSerialize)
                    .AsImplementedInterfaces()
                    .SingleInstance();
            });
        }
    }
}

んで、ポイントはKeyedメソッドでFilberModule.Key_DoNotSerializeを指定しておくというところです。シリアライズしないでねっていう印みたいです。あとは、IDialog<object>で会話の起点となるルートのDialogを登録しておきます。

RootDialogを以下のように書き換えてGreetServiceをさくっと使うようにしてみました。

using System;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using DITestBotApp.Services;

namespace DITestBotApp.Dialogs
{
    [Serializable]
    publicclass RootDialog : IDialog<object>
    {
        private IGreetService GreetService { get; }

        // DIするpublic RootDialog(IGreetService greetService)
        {
            this.GreetService = greetService;
        }

        public Task StartAsync(IDialogContext context)
        {
            context.Wait(this.GreetAsync);

            return Task.CompletedTask;
        }

        private async Task GreetAsync(IDialogContext context, IAwaitable<object> result)
        {
            // DIしたやつを使う
            await context.PostAsync(this.GreetService.GetMessage());
            await this.MessageReceivedAsync(context, result);
        }

        private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
        {
            var activity = await result as Activity;

            // calculate something for us to returnint length = (activity.Text ?? string.Empty).Length;

            // return our reply to the user
            await context.PostAsync($"You sent {activity.Text} which was {length} characters");

            context.Wait(MessageReceivedAsync);
        }
    }
}

あとは、MessagesControllerでDIコンテナからDialogを作って渡すように書き換えます。

public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
    if (activity.Type == ActivityTypes.Message)
    {
        using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, activity))
        {
            var dialog = scope.Resolve<IDialog<object>>();
            await Conversation.SendAsync(activity, () => dialog);
        }
    }
    else
    {
        HandleSystemMessage(activity);
    }
    var response = Request.CreateResponse(HttpStatusCode.OK);
    return response;
}

実行してみるとこんな感じに動きます。ちゃんとDIしたやつがいけてますね。

f:id:okazuki:20170705141734p:plain

DialogからDialogを使うケース

DialogからDialogを使うときは、新たにDialogのインスタンスを作ってIDialogContextクラスのCallメソッドに渡してやる必要があります。これはDialogを作るファクトリを作って、こいつがAutofacのクラスを使ってインスタンス作って返す感じです。やってみましょう。

こんなDialogを用意します。

using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using System;
using System.Threading.Tasks;

namespace DITestBotApp.Dialogs
{
    [Serializable]
    publicclass SimpleDialog : IDialog<object>
    {
        public async Task StartAsync(IDialogContext context)
        {
            await context.PostAsync("SimpleDialog started");
            context.Wait(this.HelloWorldAsync);
        }

        private async Task HelloWorldAsync(IDialogContext context, IAwaitable<object> result)
        {
            var input = await result as Activity;
            await context.PostAsync($"Hello world!! {input.Text}");
            context.Done<object>(null);
        }
    }
}

Global.asax.csで、こいつを登録します。

using Autofac;
using DITestBotApp.Dialogs;
using DITestBotApp.Services;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Internals.Fibers;
using System.Web.Http;

namespace DITestBotApp
{
    publicclass WebApiApplication : System.Web.HttpApplication
    {
        protectedvoid Application_Start()
        {
            GlobalConfiguration.Configure(WebApiConfig.Register);

            Conversation.UpdateContainer(builder =>
            {
                builder.RegisterType<RootDialog>()
                    .As<IDialog<object>>()
                    .InstancePerLifetimeScope();

                builder.RegisterType<SimpleDialog>()
                    .InstancePerDependency();

                builder.RegisterType<GreetService>()
                    .Keyed<IGreetService>(FiberModule.Key_DoNotSerialize)
                    .AsImplementedInterfaces()
                    .SingleInstance();
            });
        }
    }
}

ファクトリのクラスを作ります。こいつはIComponentContext(Autofacのインターフェース)を使ってインスタンスを作ります。

using Autofac;

namespace DITestBotApp.Factories
{
    publicinterface IDialogFactory
    {
        T Create<T>();
    }

    publicclass DialogFactory : IDialogFactory
    {
        private IComponentContext Scope { get; }

        public DialogFactory(IComponentContext scope)
        {
            this.Scope = scope;
        }

        public T Create<T>()
        {
            returnthis.Scope.Resolve<T>();
        }
    }
}

これも、DIコンテナに登録します。

using Autofac;
using DITestBotApp.Dialogs;
using DITestBotApp.Factories;
using DITestBotApp.Services;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Internals.Fibers;
using System.Web.Http;

namespace DITestBotApp
{
    publicclass WebApiApplication : System.Web.HttpApplication
    {
        protectedvoid Application_Start()
        {
            GlobalConfiguration.Configure(WebApiConfig.Register);

            Conversation.UpdateContainer(builder =>
            {
                builder.RegisterType<DialogFactory>()
                    .Keyed<IDialogFactory>(FiberModule.Key_DoNotSerialize)
                    .AsImplementedInterfaces()
                    .InstancePerLifetimeScope();

                builder.RegisterType<RootDialog>()
                    .As<IDialog<object>>()
                    .InstancePerLifetimeScope();

                builder.RegisterType<SimpleDialog>()
                    .InstancePerDependency();

                builder.RegisterType<GreetService>()
                    .Keyed<IGreetService>(FiberModule.Key_DoNotSerialize)
                    .AsImplementedInterfaces()
                    .SingleInstance();
            });
        }
    }
}

では、RootDialogでSimpleDialogを作るようにしてみよう。

using System;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using DITestBotApp.Services;
using Autofac;
using DITestBotApp.Factories;

namespace DITestBotApp.Dialogs
{
    [Serializable]
    publicclass RootDialog : IDialog<object>
    {
        private IGreetService GreetService { get; }
        private IDialogFactory Factory { get; }

        // DIするpublic RootDialog(IDialogFactory factory, IGreetService greetService)
        {
            this.Factory = factory;
            this.GreetService = greetService;
        }

        public Task StartAsync(IDialogContext context)
        {
            context.Wait(this.GreetAsync);

            return Task.CompletedTask;
        }

        private async Task GreetAsync(IDialogContext context, IAwaitable<object> result)
        {
            // DIしたやつを使う
            await context.PostAsync(this.GreetService.GetMessage());
            await this.MessageReceivedAsync(context, result);
        }

        private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
        {
            var activity = await result as Activity;

            if (activity.Text == "change")
            {
                context.Call(this.Factory.Create<SimpleDialog>(), this.ResumeSimpleDialogAsync);
            }
            else
            {
                // calculate something for us to returnint length = (activity.Text ?? string.Empty).Length;

                // return our reply to the user
                await context.PostAsync($"You sent {activity.Text} which was {length} characters");

                context.Wait(MessageReceivedAsync);
            }
        }

        private async Task ResumeSimpleDialogAsync(IDialogContext context, IAwaitable<object> result)
        {
            await context.PostAsync("returned");
            context.Wait(this.MessageReceivedAsync);
        }
    }
}

因みに、最初はDialogFactory作らずにIComponentContextを直接RootDialogにDIしてやればいいやって思ったのですが、この人をDIさせるとシリアライズできないと怒られてしまいましたので、1枚ラップして明示的にシリアライズしないよってマークしたやつをRootDialogにDIするようにしました。

実行してみましょう。

f:id:okazuki:20170705151116p:plain

いい感じですね。

ソースコードは以下の場所に置いてあります。

github.com


Viewing all articles
Browse latest Browse all 1387

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>