Botbuilder-dotnet: Accessing bot state in general dialog / middleware

Created on 25 Aug 2018  Â·  14Comments  Â·  Source: microsoft/botbuilder-dotnet

I am trying to update some of the open source dialogs / middleware we have as part of the community project to use the latest daily build. Right now I have a dialog which needs to store / retrieve some values from state.

When writing the dialog against the current preview packages, I had the following code, which worked.

var state = context.GetConversationState<Dictionary<string, object>>();
state[FavoritesKey] = favorites;

It now looks like (in latest) this extension method has been removed and also that the new approach is to add a state accessor service to your bot to access conversation state. How do I go about storing / retrieving state when in a generic dialog / piece of middleware which will be released as an isolated NuGet package and therefore knows nothing about the bot that is using it?

Is there a sample for this scenario anywhere?

cc'ing @drub0y as you might have an answer :)

backlog

Most helpful comment

In the example above with adding the accessor as a singleton, is there a possible way to get the accessor in the middleware? It seems impossible right now. Middleware is declared in the bot options while the accessor is added in as a singleton after.

All 14 comments

Yes, I have raised the issue that this design requires that the bot author has to know each and every TState of any component they are going to use up front so that they can created the appropriate typed accessor for each one. Additionally, they have to understand which _scope_ to create the property accessor for in order for that component to function correctly. I actually spoke to this in #773 when the accessor style API first landed.

Regarding the dialog level of the APIs, I guess one argument that could be made there is that your always only registering an accessor for the top level DialogState and from there all the state for each dialog instance is exposed as a Dictionary<string, object> (see DialogInstance::State).

When using dialogs, you are no longer _supposed_ to manage your own state directly via the lower level state management APIs (no matter what shape they take). I would argue this is fair as dialogs represent a higher level abstraction that needs to manage certain aspects of state itself to provide the conversational behaviors it offers.

Certainly one take away from this discussion should be that there should be some explicit documentation around the topic of dialogs and state that explains this so people know not to mix the use of the two APIs together by default and perhaps even some documentation on when it might make sense (e.g. a dialog accessing some user or bot state).

The goal for using an property accessor is to
• Give a developer the ability to control namespace collisions when consuming third party components
• Give the developer the ability to control the kind of storage and policy around it

The way the sample was written had all of the property accessor defined up front, which I don't think is the right way to do it. You should be defining properties in the components in a way that fosters
their use and scope.

The guidance that we will be adopting for docs and our samples is following:

For internal component state:
• Initialize property accessors in your constructor
• Give ability for custom name to act as namespace qualifier

Example of internal state properties:

public class MyDialog
{
       private IStatePropertyAccessor<Foo> property1;
       private IStatePropertyAccessor<Bar> property2;

       public MyDialog(BotState state, string name = null)
       {
              this.property1 = state.CreateProperty<Foo>($"{name ?? nameof(MyDialog)}.{nameof(property1)}");
              this.property2 = state.CreateProperty<Bar>($"{name ?? nameof(MyDialog)}.{nameof(property2)}");
       }
}

To consume external state properties:
• pass a StatePropertyAccessor either through ctor or property settor

public class MyDialog
{
       private IStatePropertyAccessor<Foo> internalProp1;
       private IStatePropertyAccessor<Bar> internalProp2;
       private IStatePropertyAccessor<Blat> externalProperty;

       public MyDialog(BotState state, IStatePropertyAccessor<Blat> externalProp, string name = null)
       {
              this.internalProp1 = state.CreateProperty<Foo>($"{name ?? nameof(MyDialog)}.{nameof(internalProp1)}");
              this.internalProp2 = state.CreateProperty<Bar>($"{name ?? nameof(MyDialog)}.{nameof(internalProp2)}");
              this.externalProperty = externalProp;
       }

       public IStatePropertyAccessor<Blat> ExternalProp { get { return this.externalProperty; } set { this.externalProperty = value; } }
}

To expose state accessor for other components to consume:
• Add property to expose your external state as a property accessor.

public class MyDialog
{
       private IStatePropertyAccessor<Foo> internalProp1;
       private IStatePropertyAccessor<Bar> internalProp2;
       private IStatePropertyAccessor<Blat> externalProperty;

       public MyDialog(BotState state, IStatePropertyAccessor<Blat> externalProp, string name = null)
       {
              this.internalProp1 = state.CreateProperty<Foo>($"{name ?? nameof(MyDialog)}.{nameof(internalProp1)}");
              this.internalProp2 = state.CreateProperty<Bar>($"{name ?? nameof(MyDialog)}.{nameof(internalProp2)}");
              this.externalProperty = externalProp;
       }

       public IStatePropertyAccessor<Foo> Prop { get { return this.internalProp1; }}
}

-Tom

Thanks @tomlm and @drub0y. I'll give my scenario a go based on the above.

Would either of you know if there is a sample on any branch that tracks against the latest changes for Dialogs? Looks like ContinueAsync was removed from the dialog context and I'm trying to figure out what I should be doing instead - this is part of rewriting a large component using waterfall dialogs to try and get it working with the latest daily build.

Thanks for you help so far.

Gary

@drub0y @tomlm Ignore the last comment - I clearly hadn't woken up yet - dialog context as it was. Stand down :) Closing this for now and will proceed with the advice above.

@tomlm @drub0y Quick follow up on this. All seems straightforward. The only thing I am unclear on now (sorry if this is obvious, but a lot seems to have shifted recently), is where I can get an instance of BotState / ConversationState to pass into my dialog. Do I need to use DI for this or is it available in some sort of services collection somewhere?

As far as I can tell, you need to use DI; however, you can also create your dialog and pass it via DI, as well. I'm not entirely clear on what the suggested best practice is yet.

services.AddBot<GreetingBot>(options =>
{
    options.CredentialProvider = new ConfigurationCredentialProvider(Configuration);
    options.OnTurnError = async (context, exception) =>
    {
        await context.TraceActivityAsync("Bot Exception", exception);
        await context.SendActivityAsync("Sorry, it looks like something went wrong!");
    };

    IStorage dataStore = new MemoryStorage();
    options.Middleware.Add(new ConversationState(dataStore));
});

// Create and register the dialog state accessor.
services.AddSingleton(sp =>
{
    var options = sp.GetRequiredService<IOptions<BotFrameworkOptions>>().Value;
    var convState = options.Middleware.OfType<ConversationState>().FirstOrDefault();
    return convState.CreateProperty<DialogState>($"DialogSet.DialogStateAccessor");
});

// Create and register the greeting dialog set.
services.AddSingleton(sp =>
{
    var accessor = sp.GetRequiredService<IStatePropertyAccessor<DialogState>>();
    return new GreetingDialogSet(accessor);
});

Thanks @JonathanFingold. I am already using DI to inject the state accessors.

What I am specifically looking for now is how to get and pass a BotState object into a child dialog as shown in Tom's example. This snippet shows this...

public MyDialog(BotState state, string name = null) {
this.internalProp1 = state.CreateProperty($"{name ?? nameof(MyDialog)}.{nameof(internalProp1)}");

Essentially allowing me to create an accessor and use it to add a property to the BotState object in a dialog (this dialog won't know anything about the bot as it will be distributed via NuGet).

@JonathanFingold @tomlm I have now got my solution working, it is a location dialog in the Community Project. Just to be clear, as I want to make sure I am following recommended patterns....

I am adding a service for my ConversationState to the services in Startup.cs so I can pass this to my bot using DI. Then, within my bot, I pass the BotState object to my dialog, which then internally creates the adds the property to the state using state.CreateProperty and has an internal IStatePropertyAccessor property.

e.g. below is a simplified snippet where my dialog takes a BotState object, creates the state property and then passes the accessor to some supporting helpers.

public LocationDialog(
            string apiKey,
            string prompt,
            BotState state,
            LocationOptions options) : base("LocationDialog")
        {
            var favoriteLocations = state.CreateProperty<List<FavoriteLocation>>($"{nameof(LocationDialog)}.Favorites");

            var favoritesManager = new FavoritesManager(favoriteLocations);

The above seems to be working, but doesn't seem that intuitive. You can see the readme I have created for this here -> https://github.com/BotBuilderCommunity/botbuilder-community-dotnet/tree/feature/track-daily-botbuilder-nuget/libraries/Bot.Builder.Community.Dialogs.Location

cc @drub0y

Thoughts

  1. This is obviously quite a bit for developers to have to figure out themselves, but, ok, the argument can be made that this can be solved by some docs/samples. The problem I see is that, because this is not intuitive, every single dev sitting down with the framework will run into this wall, have to stumble across the solution and then continually solve it on a repetitive basis; this just seems like a smell.
  2. State API wise, calling CreateProperty for the "same" property on a BotState instance that is technically a singleton on every turn and it actually working is unintuitive. I know it works, but I don't think it should. What if two completely different components decide they want to call CreateProperty for the same name? Right now it "just works" and two components can easily step on each other. IMHO once the property is established with the BotState it should be cached (within the BotState instance) and an exception thrown once another call comes in to register a property with the same name.
  3. This is just to get _one_ BotState instance injected down into _one_ Dialog subclass. Extrapolate this approach out: every single dependency an entire dialog tree needs would have to be injected down through the IBot and then the bot will have to be manually plumb those through to the individual dialogs from there.
  4. Instantiating an entire dialog tree on every turn sure seems like performance suicide, let alone all the dependencies it might have. Again, my understanding from @Stevenic and @tomlm was that the dialog trees were meant to be created as singletons themselves and then simply executed with the proper context on every turn. Honestly, it's the only way I see the API really "working" from a performance perspective.

Proposal

The Dialogs API should not be (strictly) based on instances of Dialog subclasses, but rather factories that produce instances of Dialog subclasses __when they are required__ (i.e. when conversation flows into one). Not only would this solve the performance issue with having to spin up an entire dialog tree (even as singleton at startup), but it would allow for DI to step in and take over the responsibility of instantiation. Not only would that light up DI, but it would also make it possible to have turn scoped instances of the dialogs and their dependencies.

Also, done right, this _would not force_ the use of DI at all because we can have simple overloads that keep the API exactly as it is with static instances if that's how people want to do it. Keeping with mantra of don't _force_ DI down people's throats, but not shutting it out either.

Would be happy to discuss what this might look like in more detail if it's something we'd want to explore.

In the example above with adding the accessor as a singleton, is there a possible way to get the accessor in the middleware? It seems impossible right now. Middleware is declared in the bot options while the accessor is added in as a singleton after.

In the example above with adding the accessor as a singleton, is there a possible way to get the accessor in the middleware? It seems impossible right now. Middleware is declared in the bot options while the accessor is added in as a singleton after.

Did you get anywhere with this? I'm having a nightmare trying to figure out if its even possible to access state in middleware ... to the point I'm about to throw the towel in as I can't even really find many people talking about this at the moment.

We have down played the role of middleware. However, we're releasing a new set of samples in a week or two that use dependency injection "more correctly", and should make using state in middleware much more straight forward.

With the caveat that the updates are still a work in progress, you can look at the samples-work-in-progress branch. You should be able extrapolate from the C# dialog samples.

We have down played the role of middleware. However, we're releasing a new set of samples in a week or two that use dependency injection "more correctly", and should make using state in middleware much more straight forward.

With the caveat that the updates are still a work in progress, you can look at the samples-work-in-progress branch. You should be able extrapolate from the C# dialog samples.

Hi, can you point me to a nodejs sample of using state both in bot logic and in middleware? Txs.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

cmayomsft picture cmayomsft  Â·  6Comments

drub0y picture drub0y  Â·  5Comments

EricDahlvang picture EricDahlvang  Â·  4Comments

ivorb picture ivorb  Â·  6Comments

brandonh-msft picture brandonh-msft  Â·  6Comments