| 3 min read
Bot Framework internally uses Autofac for dependency injection. If you take a close look at SDK, the top level composition root class Conversation
provides a static instance of Autofac's IContainer
. I have highlighted the code snipped from Microsoft's BotBuilder repository for curious mind in you. Head over to Conversation.cs. This makes it possible for a Bot Builder to register and resolve components.
We can seamlessly integrate our own registration module for Autofac on top of BotBuilder's implementation. If you are really interested, you can have a look what how BotBuilder internally handles scope creation and resolution. BotBuilder's DialogModule has it's own static method called BeginLifetimeScope which returns a new tagged sub-scope typeof(Dialog)
with a resolved IMessageActivity
. (refer highlighted part from DialogModule.cs).
Let's see how can we seamlessly implement Autofac dependency injection in Bot Application.
It is always better to create a bot specific separate registration module for Autofac. We can do it by extending Autofac.Module class and overriding the Load method.
public class MyBotModules : Module | |
{ | |
protected override void Load(ContainerBuilder builder) | |
{ | |
base.Load(builder); | |
//Register RootDialog as IDialog<object> | |
builder.RegisterType<RootDialog>() | |
.As<IDialog<object>>() | |
.InstancePerDependency(); | |
//We will come to this later | |
builder.RegisterType<DialogFactory>() | |
.Keyed<IDialogFactory>(FiberModule.Key_DoNotSerialize) | |
.AsImplementedInterfaces() | |
.InstancePerLifetimeScope(); | |
//Register Dialogs | |
builder.RegisterType<UserProfileDialog>().InstancePerDependency(); | |
builder.RegisterType<UserSettingsDialog>().InstancePerDependency(); | |
//Register bot specific services, make sure you Key them as DoNotSerialize | |
builder.RegisterType<DailyStatusService>() | |
.Keyed<IDailyStatusService>(FiberModule.Key_DoNotSerialize) | |
.AsImplementedInterfaces() | |
.InstancePerDependency(); | |
//Register | |
builder.RegisterType<HttpClient>() | |
.Keyed<HttpClient>(FiberModule.Key_DoNotSerialize) | |
.AsSelf() | |
.InstancePerDependency(); | |
} | |
} |
In the code snippet above we can see the registrations for RootDialog (as IDialog
With Dependency Injection in place, you can now inject an external service if it is required in any Dialog implementation.
[Serializable] | |
public class RootDialog : IDialog<object> | |
{ | |
private IProfileService profileService; | |
public RootDialog(IProfileService profileService) | |
{ | |
this.profileService = profileService; | |
} | |
//code removed for sanity. | |
} |
Without DI in place, you would start the conversation with new instance of RootDialog. Like shown below -
/// <summary> | |
/// POST: api/Messages | |
/// Receive a message from a user and reply to it | |
/// </summary> | |
public async Task<HttpResponseMessage> Post([FromBody]Activity activity) | |
{ | |
if (activity.Type == ActivityTypes.Message) | |
{ | |
await Conversation.SendAsync(activity, () => new Dialogs.RootDialog()); | |
} |
But as you have services being injected in RootDialog now. You need to resolve all the services before creating a new instance of RootDialog. This approach doesn't look ideal. Now we can make use of our good old friend Conversation.Container to get the activity scope and resolve root dialog from that scope.
/// <summary> | |
/// POST: api/Messages | |
/// Receive a message from a user and reply to it | |
/// </summary> | |
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); | |
} | |
} | |
. | |
. | |
//rest of the code removed for sanity.. | |
} |
Resolving Dialog from activity scope will resolve it's dependent services too. In our case ProfileService is injected in RootDialog and HttpClient is injected in ProfileService.
Beginning conversation can be handled with tagged sub-scope. But as soon as you are within a conversation and bot has a logic to call a child dialog or forward the conversation to another dialog, we need to resolve the dialog. We can use IDialogFactory implementation for that. DialogFactory injects IComponentContext. Autofac automatically provides IComponentContext instance which is a scope within application. We can use this scope to resolve Dialog.
public interface IDialogFactory | |
{ | |
//constructor without dynamic params | |
T Create<T>(); | |
//Constructor requiring multiple dynamic params | |
T Create<T>(IDictionary<string, object> parameters); | |
} | |
public class DialogFactory : IDialogFactory | |
{ | |
protected readonly IComponentContext Scope; | |
//IComponentContext does not need to be registered as Autofac automatically provides it. | |
public DialogFactory(IComponentContext scope) | |
{ | |
SetField.NotNull(out this.Scope, nameof(scope), scope); | |
} | |
public T Create<T>() | |
{ | |
return this.Scope.Resolve<T>(); | |
} | |
public T Create<T>(IDictionary<string, object> parameters) | |
{ | |
return this.Scope.Resolve<T>(parameters.Select(kv => new NamedParameter(kv.Key, kv.Value))); | |
} | |
} |
We need to inject IDialogFactory in required Dialog class. It will be used to create a child dialog later on.
[Serializable] | |
public class RootDialog : IDialog<object> | |
{ | |
private ConversationReference conversationReference; | |
private IDialogFactory dialogFactory; | |
public RootDialog(IDialogFactory dialogFactory) | |
{ | |
this.dialogFactory = dialogFactory; | |
} | |
public async Task StartAsync(IDialogContext context) | |
{ | |
context.Wait(this.MessageReceivedAsync); | |
} | |
public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> item) | |
{ | |
var message = await item; | |
context.Call(this.dialogFactory.Create<ProfileDialog>(), this.AfterProfileDialog); | |
} | |
private async Task AfterDailyStatusDialogAsync(IDialogContext context, IAwaitable<object> result) | |
{ | |
context.Done("Thanks"); | |
} | |
} |
If we have a dialog with custom parameters like string. For example, if newly created identifier is needed in a dialog. We have to pass it as a parameter. We have a provision in our DialogFactory for such incidences. This is how we can do it -
Dictionary<string, object> dict = new Dictionary<string, object>(); | |
dict.Add("ticket_id", "this is string"); | |
dict.Add("order", this.order); | |
context.Call(this.dialogFactory.Create<OrderSummaryDialog, Dictionary<string, object>>(dict), this.AfterOrderSummaryDialog); |
Your bot application should make full use of Dependency Injection goodness now. With this one time effort you can make your bot application testable, maintainable and most importantly scalable.
Good-bye, until then. Rahul.