Save user and conversation data

Note

This topic is for the latest release of the SDK (v4). You can find content for the older version of the SDK (v3) here.

A bot is inherently stateless. Once your bot is deployed, it may not run in the same process or on the same machine from one turn to the next. However, your bot may need to track the context of a conversation, so that it can manage its behavior and remember answers to previous questions. The state and storage features of the SDK allow you to add state to your bot.

Prerequisites

About the sample code

This article discusses the configuration aspects of managing your bot's state. To add state, we configure state properties, state management, and storage, and then use those in the bot.

  • Each state property contains state information for your bot.
  • Each state property accessor allows you to get or set the value of the associated state property.
  • Each state management object automates the reading and writing of associated state information to storage.
  • The storage layer connects to the backing storage for state, such as in-memory (for testing), or Azure Cosmos DB Storage (for production).

We need to configure the bot with state property accessors with which it can get and set state at run-time, when it is handling an activity. A state property accessor is created using a state management object, and a state management object is created using a storage layer. So, we'll start at the storage level and work up from there.

Configure storage

Since we don't plan to deploy this bot, we'll use memory storage, which we'll use this to configure both user and conversation state in the next step.

In Startup.cs, configure the storage layer.

public void ConfigureServices(IServiceCollection services)
{
    // ...
    IStorage storage = new MemoryStorage();
    // ...
}

Create state management objects

We track both user and conversation state, and use these to create state property accessors in the next step.

In Startup.cs, reference the storage layer when you create your state management objects.

public void ConfigureServices(IServiceCollection services)
{
    // ...
    ConversationState conversationState = new ConversationState(storage);
    UserState userState = new UserState(storage);
    // ...
}

Create state property accessors

To declare a state property, first create a state property accessor, using one of our state management objects. We configure the bot to track the following information:

  • The user's name, which we'll define in user state.
  • Whether we've just asked the user for their name and some additional information about the message they just sent.

The bot uses the accessor to get the state property from the turn context.

We first define classes to contain all the information that we want to manage in each type of state.

  • A UserProfile class for the user information that the bot will collect.
  • A ConversationData class to track information about when a message arrived and who sent the message.
// Defines a state property used to track information about the user.
public class UserProfile
{
    public string Name { get; set; }
}
// Defines a state property used to track conversation data.
public class ConversationData
{
    // The time-stamp of the most recent incoming message.
    public string Timestamp { get; set; }

    // The ID of the user's channel.
    public string ChannelId { get; set; }

    // Track whether we have already asked the user's name
    public bool PromptedUserForName { get; set; } = false;
}

Next, we define a class that contains the state management information we'll need to configure our bot instance.

public class StateBotAccessors
{
    public StateBotAccessors(ConversationState conversationState, UserState userState)
    {
        ConversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState));
        UserState = userState ?? throw new ArgumentNullException(nameof(userState));
    }
  
    public static string UserProfileName { get; } = "UserProfile";

    public static string ConversationDataName { get; } = "ConversationData";

    public IStatePropertyAccessor<UserProfile> UserProfileAccessor { get; set; }

    public IStatePropertyAccessor<ConversationData> ConversationDataAccessor { get; set; }
  
    public ConversationState ConversationState { get; }
  
    public UserState UserState { get; }
}

Configure your bot

Now we're ready to define the state property accessors and configure our bot. We'll use the conversation state management object for the conversation flow state property accessor. We'll use the user state management object for the user profile state property accessor.

In Startup.cs, we configure ASP.NET to provide the bundled state property and management objects. This will be retrieved from the bot's constructor through the dependency injection framework in ASP.NET Core.

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddSingleton<StateBotAccessors>(sp =>
    {
        // Create the custom state accessor.
        return new StateBotAccessors(conversationState, userState)
        {
            ConversationDataAccessor = conversationState.CreateProperty<ConversationData>(StateBotAccessors.ConversationDataName),
            UserProfileAccessor = userState.CreateProperty<UserProfile>(StateBotAccessors.UserProfileName),
        };
    });
}

In the bot's constructor, the CustomPromptBotAccessors object is provided when ASP.NET creates the bot.

// Defines a bot for filling a user profile.
public class CustomPromptBot : IBot
{
    private readonly StateBotAccessors _accessors;

    public StateBot(StateBotAccessors accessors, ILoggerFactory loggerFactory)
    {
        // ...
        accessors = accessors ?? throw new System.ArgumentNullException(nameof(accessors));
    }

    // The bot's turn handler and other supporting code...
}

Access state from your bot

The preceding sections cover the initialization-time steps to add our state property accessors to our bot. Now, we can use those accessors at run-time to read and write state information.

  1. Before we use our state properties, we use each accessor to load the property from storage and get it from the state cache.
    • Whenever you get a state property via its accessor, you should provide a default value. Otherwise, you can get a null value error.
  2. Before we exit the turn handler:
    1. We use the accessors' set method to push changes to the bot state.
    2. We use the state management objects' save changes method to write those changes to storage.
// The bot's turn handler.
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
    if (turnContext.Activity.Type == ActivityTypes.Message)
    {
        // Get the state properties from the turn context.
        UserProfile userProfile =
            await _accessors.UserProfileAccessor.GetAsync(turnContext, () => new UserProfile());
        ConversationData conversationData =
            await _accessors.ConversationDataAccessor.GetAsync(turnContext, () => new ConversationData());

        if (string.IsNullOrEmpty(userProfile.Name))
        {
            // First time around this is set to false, so we will prompt user for name.
            if (conversationData.PromptedUserForName)
            {
                // Set the name to what the user provided
                userProfile.Name = turnContext.Activity.Text?.Trim();

                // Acknowledge that we got their name.
                await turnContext.SendActivityAsync($"Thanks {userProfile.Name}.");

                // Reset the flag to allow the bot to go though the cycle again.
                conversationData.PromptedUserForName = false;
            }
            else
            {
                // Prompt the user for their name.
                await turnContext.SendActivityAsync($"What is your name?");

                // Set the flag to true, so we don't prompt in the next turn.
                conversationData.PromptedUserForName = true;
            }

            // Save user state and save changes.
            await _accessors.UserProfileAccessor.SetAsync(turnContext, userProfile);
            await _accessors.UserState.SaveChangesAsync(turnContext);
        }
        else
        {
            // Add message details to the conversation data.
            conversationData.Timestamp = turnContext.Activity.Timestamp.ToString();
            conversationData.ChannelId = turnContext.Activity.ChannelId.ToString();

            // Display state data
            await turnContext.SendActivityAsync($"{userProfile.Name} sent: {turnContext.Activity.Text}");
            await turnContext.SendActivityAsync($"Message received at: {conversationData.Timestamp}");
            await turnContext.SendActivityAsync($"Message received from: {conversationData.ChannelId}");
        }

        // Update conversation state and save changes.
        await _accessors.ConversationDataAccessor.SetAsync(turnContext, conversationData);
        await _accessors.ConversationState.SaveChangesAsync(turnContext);
    }
}

Test the bot

  1. Run the sample locally on your machine. If you need instructions, refer to the README file for C# or JS sample.
  2. Use the emulator to test the bot as shown below.

test state bot sample

Additional resources

Privacy: If you intend to store user's personal data, you should ensure compliance with General Data Protection Regulation.

State management: All of the state management calls are asynchronous, and last-writer-wins by default. In practice, you should get, set, and save state as close together in your bot as possible.

Critical business data: Use bot state to store preferences, user name, or the last thing they ordered, but do not use it to store critical business data. For critical data, create your own storage components or write directly to storage.

Recognizer-Text: The sample uses the Microsoft/Recognizers-Text libraries to parse and validate user input. For more information, see the overview page.

Next step

Now that you know how to configure state to help you read and write bot data to storage, let's learn how ask the user a series of questions, validate their answers, and save their input.