Persist user 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.

When the bot ask users for input, chances are that you would want to persist some of the information to storage of some form. The Bot Builder SDK allows you to store user inputs using in-memory storage or database storage such as CosmosDB. Local storage types are mainly used during testing or prototyping of your bot. However, persistent storage types, such as database storage, are best for production bots.

This topic shows you how to define your storage object and save user inputs to the storage object so that it can be persisted. We'll use a dialog to ask the user for their name, if we don't already have it. Regardless of the storage type you choose to use, the process for hooking it up and persisting data is the same. The code in this topic uses CosmosDB as the storage to persist data.

Prerequisites

Certain resources are required, based on the development environment you want to use.

This tutorial makes use of the following packages.

Install these packages from the NuGet packet manager.

  • Microsoft.Bot.Builder.Azure
  • Microsoft.Bot.Builder.Dialogs
  • Microsoft.Bot.Builder.Integration.AspNet.Core

To test the bot you create in this tutorial, you will need to install the BotFramework Emulator.

Create a CosmosDB service and update your application settings

To set up a CosmosDB service and a database, follow the instructions for using CosmosDB. The steps are summarized here:

  1. In a new browser window, sign in to the Azure portal.
  2. Click Create a resource > Databases > Azure Cosmos DB.
  3. In the New account page, provide a unique name in the ID field. For API, select SQL, and provide Subscription, Location, and Resource group information.
  4. Click Create.

Then, Add a collection to that service for use with this bot.

Record the database ID and collection ID you used to add the collection, and also the URI and primary key from the collection's keys settings, as we will need these to connect our bot to the service.

Update your application settings

Update your appsettings.json file to include the connection information for CosmosDB .

{
  // Settings for CosmosDB.
  "CosmosDB": {
    "DatabaseID": "<your-database-identifier>",
    "CollectionID": "<your-collection-identifier>",
    "EndpointUri": "<your-CosmosDB-endpoint>",
    "AuthenticationKey": "<your-primary-key>"
  }
}

Create storage, state manager, and state property accessor objects

Bots use state management and storage objects to manage and persist state. The manager provides an abstraction layer that lets you access state properties using state property accessors, independent of the type of underlying storage. Use the state manager to write data to storage.

Define a class for your user data

Rename the file CounterState.cs to UserData.cs, and rename the CounterState class to UserData.

Update this class to hold the data you will collect.

/// <summary>
/// Class for storing persistent user data.
/// </summary>
public class UserData
{
    public string Name { get; set; }
}

Define a class for your state and state property accessor objects

Rename the file EchoBotAccessors.cs to BotAccessors.cs, and rename the EchoBotAccessors class to BotAccessors.

Update this class to store the state objects and state property accessors your bot will need.

using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using System;

public class BotAccessors
{
    public UserState UserState { get; }

    public ConversationState ConversationState { get; }

    public IStatePropertyAccessor<DialogState> DialogStateAccessor { get; set; }

    public IStatePropertyAccessor<UserData> UserDataAccessor { get; set; }

    public BotAccessors(UserState userState, ConversationState conversationState)
    {
        this.UserState = userState
            ?? throw new ArgumentNullException(nameof(userState));

        this.ConversationState = conversationState
            ?? throw new ArgumentNullException(nameof(conversationState));
    }
}

Update the startup code for your bot

In your Startup.cs file, update your using statements.

using System;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Azure;
using Microsoft.Bot.Builder.BotFramework;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Integration;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Builder.TraceExtensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

In your ConfigureServices method, update the add bot call, starting from where you create the backing storage object, and then register your bot accessors object.

We need conversation state for the DialogState object to track the dialog state. We're registering singletons for the dialog state property accessor and the dialog set that our bot will use. The bot will create its own state property accessor for the user state.

The BotAccessors accessor is an efficient way to manage storage for multiple state objects of your bot.

public void ConfigureServices(IServiceCollection services)
{
    // Register your bot.
    services.AddBot<UserDataBot>(options =>
    {
        // ...

        // Use persistent storage and create state management objects.
        var CosmosSettings = Configuration.GetSection("CosmosDB");
        IStorage storage = new CosmosDbStorage(
            new CosmosDbStorageOptions
            {
                DatabaseId = CosmosSettings["DatabaseID"],
                CollectionId = CosmosSettings["CollectionID"],
                CosmosDBEndpoint = new Uri(CosmosSettings["EndpointUri"]),
                AuthKey = CosmosSettings["AuthenticationKey"],
            });
        options.State.Add(new ConversationState(storage));
        options.State.Add(new UserState(storage));
    });

    // Register the bot's state and state property accessor objects.
    services.AddSingleton<BotAccessors>(sp =>
    {
        var options = sp.GetRequiredService<IOptions<BotFrameworkOptions>>().Value;
        var userState = options.State.OfType<UserState>().FirstOrDefault();
        var conversationState = options.State.OfType<ConversationState>().FirstOrDefault();

        return new BotAccessors(userState, conversationState)
        {
            UserDataAccessor = userState.CreateProperty<UserData>("UserDataBot.UserData"),
            DialogStateAccessor = conversationState.CreateProperty<DialogState>("UserDataBot.DialogState"),
        };
    });
}

When it comes time to save user data, you have some choices. The SDK provides a few state objects with different scopes that you can choose from. Here, we're using conversation state to manage the dialog state object and user state to manage user data.

For more information about state in general, see state and storage and how to manage conversation and user state.

Create a greeting dialog

We'll use a dialog to collect the user's name. To keep this scenario simple, the dialog will return the user's name, and the bot will manage the user data object and state.

Create a GreetingsDialog class and include the following code.

using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;

/// <summary>Defines a dialog for collecting a user's name.</summary>
public class GreetingsDialog : DialogSet
{
    /// <summary>The ID of the main dialog.</summary>
    public const string MainDialog = "main";

    /// <summary>The ID of the the text prompt to use in the dialog.</summary>
    private const string TextPrompt = "textPrompt";

    /// <summary>Creates a new instance of this dialog set.</summary>
    /// <param name="dialogState">The dialog state property accessor to use for dialog state.</param>
    public GreetingsDialog(IStatePropertyAccessor<DialogState> dialogState)
        : base(dialogState)
    {
        // Add the text prompt to the dialog set.
        Add(new TextPrompt(TextPrompt));

        // Define the main dialog and add it to the set.
        Add(new WaterfallDialog(MainDialog, new WaterfallStep[]
        {
            async (stepContext, cancellationToken) =>
            {
                // Ask the user for their name.
                return await stepContext.PromptAsync(TextPrompt, new PromptOptions
                {
                    Prompt = MessageFactory.Text("What is your name?"),
                }, cancellationToken);
            },
            async (stepContext, cancellationToken) =>
            {
                // Assume that they entered their name, and return the value.
                return await stepContext.EndDialogAsync(stepContext.Result, cancellationToken);
            },
        }));
    }
}

For more information about dialogs, see how to prompt for input and how to manage simple conversation flow.

Update your bot to use the dialog and user state

We'll discuss bot construction and managing user input separately.

Add the dialog and a user state accessor

We need to track the dialog instance and the state property accessor for user data.

Add code to initialize our bot.

using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;

/// <summary>Defines the bot for the persisting user data tutorial.</summary>
public class UserDataBot : IBot
{
    /// <summary>The bot's state and state property accessor objects.</summary>
    private BotAccessors Accessors { get; }

    /// <summary>The dialog set that has the dialog to use.</summary>
    private GreetingsDialog GreetingsDialog { get; }

    /// <summary>Create a new instance of the bot.</summary>
    /// <param name="options">The options to use for our app.</param>
    /// <param name="greetingsDialog">An instance of the dialog set.</param>
    public UserDataBot(BotAccessors botAccessors)
    {
        // Retrieve the bot's state and accessors.
        Accessors = botAccessors;

        // Create the greetings dialog.
        GreetingsDialog = new GreetingsDialog(Accessors.DialogStateAccessor);
    }
}

Update the turn handler

The turn handler will greet the user when they first join the conversation, and respond to them whenever they send the bot a message. If at any point the bot doesn't know the user's name already, it will start the greeting dialog to ask for their name. When the dialog completes, we'll save their name to user state using a state object generated by our state property accessor. When the turn ends, the accessor and state manager will write changes to the object out to storage.

We'll also add support for the delete user data activity.

Update the bot's OnTurnAsync method.

/// <summary>Handles incoming activities to the bot.</summary>
/// <param name="turnContext">The context object for the current turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
    // Retrieve user data from state.
    UserData userData = await Accessors.UserDataAccessor.GetAsync(turnContext, () => new UserData());

    // Establish context for our dialog from the turn context.
    DialogContext dc = await GreetingsDialog.CreateContextAsync(turnContext);

    // Handle conversation update, message, and delete user data activities.
    switch (turnContext.Activity.Type)
    {
        case ActivityTypes.ConversationUpdate:

            // Greet any user that is added to the conversation.
            IConversationUpdateActivity activity = turnContext.Activity.AsConversationUpdateActivity();
            if (activity.MembersAdded.Any(member => member.Id != activity.Recipient.Id))
            {
                if (userData.Name is null)
                {
                    // If we don't already have their name, start a dialog to collect it.
                    await turnContext.SendActivityAsync("Welcome to the User Data bot.");
                    await dc.BeginDialogAsync(GreetingsDialog.MainDialog);
                }
                else
                {
                    // Otherwise, greet them by name.
                    await turnContext.SendActivityAsync($"Hi {userData.Name}! Welcome back to the User Data bot.");
                }
            }

            break;

        case ActivityTypes.Message:

            // If there's a dialog running, continue it.
            if (dc.ActiveDialog != null)
            {
                var dialogTurnResult = await dc.ContinueDialogAsync();
                if (dialogTurnResult.Status == DialogTurnStatus.Complete
                    && dialogTurnResult.Result is string name
                    && !string.IsNullOrWhiteSpace(name))
                {
                    // If it completes successfully and returns a valid name, save the name and greet the user.
                    userData.Name = name;
                    await turnContext.SendActivityAsync($"Pleased to meet you {userData.Name}.");
                }
            }
            // Else, if we don't have the user's name yet, ask for it.
            else if (userData.Name is null)
            {
                await dc.BeginDialogAsync(GreetingsDialog.MainDialog);
            }
            // Else, echo the user's message text.
            else
            {
                await turnContext.SendActivityAsync($"{userData.Name} said, '{turnContext.Activity.Text}'.");
            }

            break;

        case ActivityTypes.DeleteUserData:

            // Delete the user's data.
            userData.Name = null;
            await turnContext.SendActivityAsync("I have deleted your user data.");

            break;
    }

    // Update the user data in the turn's state cache.
    await Accessors.UserDataAccessor.SetAsync(turnContext, userData, cancellationToken);

    // Persist any changes to storage.
    await Accessors.UserState.SaveChangesAsync(turnContext, false, cancellationToken);
    await Accessors.ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
}

Start your bot in Visual Studio

Build and run your application.

Start the emulator and connect your bot

Next, start the emulator and then connect to your bot in the emulator:

  1. Click the Open Bot link in the emulator "Welcome" tab.
  2. Select the .bot file located in the directory where you created the Visual Studio solution.

Interact with your bot

Send a message to your bot, and the bot will respond back with a message. Emulator running

Next steps