Accessing a .NET bot's state via dependency injection
[NOTE: This post refers to version 3 of the Bot Framework]
When using the .NET BotBuilder SDK’s dialog system, you can access the bot state using the dialog context, but what if you don’t have the dialog context handy?
You have two options
- Pass the context around all the time
- Use the already built in IoC container to get the bot state.
With v3 of the Microsoft Bot Builder SDK, the documentation describes how to configure your bot to store its bot state data in either Azure Table storage or in a Cosmos DB. Notice that in the documentation, you configure the storage providers via Autofac registration (which Bot Builder uses internally to manage its various services). So you should be able to get them back from the Autofac container via dependency injection.
To move into a dependency injection model you need to load your dialogs from the dependency container too. That way, any dependencies (like the bot state) will be injected automatically.
In this example I have a few key components to demonstrate the pattern
- IBotData - this interface is defined, implemented, and registered by the BotBuilder SDK automatically. Whenever you post an Activity to a dialog chain, the SDK loads the IBotData, process the post, then saves the IBotData. So if we put a dependency on this, we will always have the current botstate loaded and available while our dialog or service is running (including user, conversation and private conversation data)
- RootDialog – this is the main dialog, it has a dependency on the IUserData service. This dialog doesn’t know anything about bot state. It just uses the IUserData service to load and save a normal .NET property. The IUserData service will be injected into the constructor parameter automatically.
- UserData – this class is a facade over the bot state. It implements the IUserData service. It depends on the IBotData service, but has no concept of dialogs or dialog contexts. This provides a strongly typed property-like experience for any consumer that want to read/write data that is persisted in bot state
- BotModule – the class that registers RootDialog and UserData with the IoC container
The BotModule registers the RootDialog class so that we can resolve it and have its dependencies injected into the constructor.
It then registers the UserData class and marks it as non-serializable. The UserData class doesn’t have any data of its own and just passes through to the IBotData service. The BotBuilder SDK will not attempt to serialize any objects registered with the key Key_DoNotSerialize.
Finally, we need to change how we invoke the RootDialog. We cannot use the traditional Conversation.SendAsync() method because inside that method, the BotBuilder SDK creates a new IoC scope. We need to have our RootDialog and the BotBuilder services in the same scope. To achieve this we will create the scope ourselves and the post to the bot in the same way that SendAsync() does.
Specifically we need to
- Create a delegate that will resolve a new RootDialog.
- Register that delegate for use by the bot (in this scope)
- Resolve the IPostToBot service.
- Post the current activity to the IPostToBot service.
Once this is all in place, the bot SDK will use our RootDialog generated from the IoC container as the root of the dialog stack. Our dialog can now read and write properties naturally and they will be read from and written to the underlying bot state provider automatically.
The full solution is available at https://github.com/negativeeddy/BotStateDIExample
(edit: added description of IBotData)