Migrate a .NET v3 bot to a .NET Core v4 bot

APPLIES TO: SDK v4

In this article we'll convert the v3 ContosoHelpdeskChatBot into a v4 bot in a new .NET Core project. This conversion is broken down into these steps:

  1. Create the new project using a template.
  2. Install additional NuGet packages as necessary.
  3. Personalize your bot, update your Startup.cs file, and update your controller class.
  4. Update your bot class.
  5. Copy over and update your dialogs and models.
  6. Final porting step.

The result of this conversion is the .NET Core v4 ContosoHelpdeskChatBot. To migrate to a .NET Framework v4 bot without converting the project type, see Migrate a .NET v3 bot to a .NET Framework v4 bot.

Bot Framework SDK v4 is based on the same underlying REST API as SDK v3. However, SDK v4 is a refactoring of the previous version of the SDK to allow developers more flexibility and control over their bots. Major changes in the SDK include:

  • State is managed via state management objects and property accessors.
  • Setting up turn handler and passing activities to it has changed.
  • Scorables no longer exist. You can check for "global" commands in the turn handler, before passing control to your dialogs.
  • A new Dialogs library that is very different from the one in the previous version. You'll need to convert old dialogs to the new dialog system using component and waterfall dialogs and the community implementation of Formflow dialogs for v4.

For more information about specific changes, see differences between the v3 and v4 .NET SDK.

Create the new project using a template

Note

The VSIX package includes both .NET Core 2.1 and .NET Core 3.1 versions of the C# templates. When creating new bots in Visual Studio 2019, you should use the .NET Core 3.1 templates. The current bot samples use .NET Core 3.1 templates. You can find the samples that use .NET Core 2.1 templates in the 4.7-archive branch of the BotBuilder-Samples repository. For information about deploying .NET Core 3.1 bots to Azure, see how to deploy your bot to Azure.

  1. If you haven't done so already, install the Bot Framework SDK v4 template for C#.
  2. Open Visual Studio, and create a new Echo Bot project from the template. Name your project ContosoHelpdeskChatBot.

Install additional NuGet packages

The template installs most of the packages you will need, including the Microsoft.Bot.Builder and Microsoft.Bot.Connector packages.

  1. Add Bot.Builder.Community.Dialogs.Formflow

    This is a community library for building v4 dialogs from v3 Formflow definition files. It has Microsoft.Bot.Builder.Dialogs as one of its dependencies, so this is also installed for us.

  2. Add log4net to support logging.

Personalize your bot

  1. Rename your bot file from Bots\EchoBot.cs to Bots\DialogBot.cs and rename the EchoBot class to DialogBot.
  2. Rename your controller from Controllers\BotController.cs to Controllers\MessagesController.cs and rename the BotController class to MessagesController.

Update your Startup.cs file

The way state and how the bot receives incoming activities has changed. We have to set up parts of the state management infrastructure ourselves in v4. For instance, v4 uses a bot adapter to handle authentication and forward activities to your bot code, and we have declare our state properties up front.

We'll create a state property for DialogState, which we now need for dialog support in v4. We'll use dependency injection to get necessary information to the controller and bot code.

In Startup.cs:

  1. Update the using statements:

    using ContosoHelpdeskChatBot.Bots;
    using ContosoHelpdeskChatBot.Dialogs;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Bot.Builder;
    using Microsoft.Bot.Builder.BotFramework;
    using Microsoft.Bot.Builder.Integration.AspNet.Core;
    using Microsoft.Bot.Connector.Authentication;
    using Microsoft.Extensions.DependencyInjection;
    
  2. Remove this constructor:

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }
    
  3. Remove the Configuration property.

  4. Update the ConfigureServices method with this code:

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        
        // Create the credential provider to be used with the Bot Framework Adapter.
        services.AddSingleton<ICredentialProvider, ConfigurationCredentialProvider>();
    
        // Create the Bot Framework Adapter with error handling enabled. 
        services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();
    
        // Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.) 
        services.AddSingleton<IStorage, MemoryStorage>();
        
        // Create the Conversation state. (Used by the Dialog system itself.)
        services.AddSingleton<ConversationState>();
    
        // Create the Root Dialog as a singleton
        services.AddSingleton<RootDialog>();
    
        // Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
        services.AddTransient<IBot, DialogBot<RootDialog>>();
    }
    

You are going to have compile time errors at this time. We'll fix them in the next steps.

MessagesController class

This class handles a request. Dependency Injection will provide the Adapter and IBot implementation at runtime. This template class is unchanged.

This is where the bot starts a turn in v4, this is quite different form the v3 message controller. Except for the bot's turn handler itself, most of this can be thought of as boilerplate.

The bot turn handler will be defined in the Bots\DialogBot.cs.

Ignore the CancelScorable and GlobalMessageHandlersBotModule classes

Since scorables don't exist in v4 we just ignore these classes. We'll update the turn handler to react to a cancel message.

Update your bot class

In v4, the turn handler or message loop logic is primarily in a bot file. We're deriving from ActivityHandler, which defines handlers for common types of activities.

  1. Update the Bots\DialogBots.cs file.

  2. Update the using statements:

    using Microsoft.Bot.Builder;
    using Microsoft.Bot.Builder.Dialogs;
    using Microsoft.Bot.Schema;
    using System.Threading;
    using System.Threading.Tasks;
    
  3. Update DialogBot to include a generic parameter for the dialog.

    public class DialogBot<T> : ActivityHandler where T : Dialog
    
  4. Add these fields and a constructor to initialize them. Again, ASP.NET uses dependency injection to get the parameters values.

    protected readonly Dialog _dialog;
    protected readonly BotState _conversationState;
    
    public DialogBot(ConversationState conversationState, T dialog)
    {
        _conversationState = conversationState;
        _dialog = dialog;
    }
    
  5. Update OnMessageActivityAsync implementation to invoke our main dialog. (We'll define the Run extension method shortly.)

protected override async Task OnMessageActivityAsync(
    ITurnContext<IMessageActivity> turnContext,
    CancellationToken cancellationToken)
{
    // Run the Dialog with the new message Activity.
    await _dialog.Run(
        turnContext,
        _conversationState.CreateProperty<DialogState>("DialogState"),
        cancellationToken);
}
  1. Update OnTurnAsync to save our conversation state at the end of the turn. In v4, we have to do this explicitly to write state out to the persistence layer. ActivityHandler.OnTurnAsync method calls specific activity handler methods, based on the type of activity received, so we save state after the call to the base method.

    public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken)
    {
        await base.OnTurnAsync(turnContext, cancellationToken);
    
        // Save any state changes that might have occured during the turn.
        await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
    }
    

Create the Run extension method

We're creating an extension method to consolidate the code needed to run a bare component dialog from our bot.

Create a DialogExtensions.cs file and implement a Run extension method.

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

namespace ContosoHelpdeskChatBot
{
    public static class DialogExtensions
    {
        public static async Task Run(this Dialog dialog, ITurnContext turnContext, IStatePropertyAccessor<DialogState> accessor, CancellationToken cancellationToken = default(CancellationToken))
        {
            var dialogSet = new DialogSet(accessor);
            dialogSet.Add(dialog);

            var dialogContext = await dialogSet.CreateContextAsync(turnContext, cancellationToken);
            var results = await dialogContext.ContinueDialogAsync(cancellationToken);
            if (results.Status == DialogTurnStatus.Empty)
            {
                await dialogContext.BeginDialogAsync(dialog.Id, null, cancellationToken);
            }
        }
    }
}

Copy over and convert your dialogs

We'll make many changes to the original v3 dialogs to migrate them to the v4 SDK. Don't worry about the compiler errors for now. These will resolve once we've finished the conversion. In the interest of not modifying the original code more than necessary, there will remain some compiler warnings after we've finished the migration.

All of our dialogs will derive from ComponentDialog, instead of implementing the IDialog<object> interface of v3.

This bot has four dialogs that we need to convert:

Dialog Description
RootDialog Presents options and starts the other dialogs.
InstallAppDialog Handles requests to install an app on a machine.
LocalAdminDialog Handles requests for local machine admin rights.
ResetPasswordDialog Handles requests to reset your password.

We won't copy over the CancelScorable class, as scorables no longer exist. You can check for global commands in the turn handler, before passing control to your dialogs.

These gather input, but do not perform any of these operations on your machine.

  1. Create a Dialogs folder in your project.
  2. Copy these files from the v3 project's dialogs directory into your new dialogs directory.
    • InstallAppDialog.cs
    • LocalAdminDialog.cs
    • ResetPasswordDialog.cs
    • RootDialog.cs

Make solution-wide dialog changes

  1. For the entire solution, Replace all occurrences of IDialog<object> with ComponentDialog.
  2. For the entire solution, Replace all occurrences of IDialogContext with DialogContext.
  3. For each dialog class, remove the [Serializable] attribute.

Control flow and messaging within dialogs are no longer handled the same way, so we'll need to revise this as we convert each dialog.

Operation v3 code v4 code
Handle the start of your dialog Implement IDialog.StartAsync Make this the first step of a waterfall dialog, or implement Dialog.BeginDialogAsync
Handle continuation of your dialog Call IDialogContext.Wait Add additional steps to a waterfall dialog, or implement Dialog.ContinueDialogAsync
Send a message to the user Call IDialogContext.PostAsync Call ITurnContext.SendActivityAsync
Start a child dialog Call IDialogContext.Call Call DialogContext.BeginDialogAsync
Signal that the current dialog has completed Call IDialogContext.Done Call DialogContext.EndDialogAsync
Get the user's input Use an IAwaitable<IMessageActivity> parameter Use a prompt from within a waterfall, or use ITurnContext.Activity

Notes about the v4 code:

  • Within dialog code, use the DialogContext.Context property to get the current turn context.
  • Waterfall steps have a WaterfallStepContext parameter, which derives from DialogContext.
  • All concrete dialog and prompt classes derive from the abstract Dialog class.
  • You assign an ID when you create a component dialog. Each dialog in a dialog set needs to be assigned an ID unique within that set.

Update the root dialog

In this bot, the root dialog prompts the user for a choice from a set of options, and then starts a child dialog based on that choice. This then loops for the lifetime of the conversation.

  • We can set the main flow up as a waterfall dialog, which is a new concept in the v4 SDK. It will run through a fixed set of steps in order. For more information, see Implement sequential conversation flow.
  • Prompting is now handled through prompt classes, which are short child dialogs that prompt for input, do some minimal processing and validation, and return a value. For more information, see gather user input using a dialog prompt.

In the Dialogs/RootDialog.cs file:

  1. Update the using statements:

    using Microsoft.Bot.Builder;
    using Microsoft.Bot.Builder.Dialogs;
    using Microsoft.Bot.Builder.Dialogs.Choices;
    using System;
    using System.Collections.Generic;
    using System.Threading;
    using System.Threading.Tasks;
    
  2. We need to convert HelpdeskOptions options from a list of strings to a list of choices. This will be used with a choice prompt, which will accept the choice number (in the list), the choice value, or any of the choice's synonyms as valid input.

    private static List<Choice> HelpdeskOptions = new List<Choice>()
        {
            new Choice(InstallAppOption) { Synonyms = new List<string> { "install" } },
            new Choice(ResetPasswordOption) { Synonyms = new List<string> { "password" } },
            new Choice(LocalAdminOption)  { Synonyms = new List<string> { "admin" } }
        };
    
  3. Add a constructor. This code does the following:

    • Each instance of a dialog is assigned an ID when it is created. The dialog ID is part of the dialog set to which the dialog is being added. Recall that the bot was initialized with a dialog object in the MessageController class. Each ComponentDialog has its own internal dialog set, with its own set of dialog IDs.
    • It adds the other dialogs, including the choice prompt, as child dialogs. Here, we're just using the class name for each dialog ID.
    • It defines a three-step waterfall dialog. We'll implement those in a moment.
      • The dialog will first prompt the user to choose a task to perform.
      • Then, start the child dialog associated with that choice.
      • And finally, restart itself.
    • Each step of the waterfall is a delegate, and we'll implement those next, taking existing code from the original dialog where we can.
    • When you start a component dialog, it will start its initial dialog. By default, this is the first child dialog added to a component dialog. We're explicitly setting the InitialDialogId property, which means that the main waterfall dialog does not need to be the first one you add to the set. For instance, if you prefer to add prompts first, this would allow you to do so without causing a run-time issue.
    public RootDialog()
        : base(nameof(RootDialog))
    {
        InitialDialogId = nameof(WaterfallDialog);
        AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
        {
            PromptForOptionsAsync,
            ShowChildDialogAsync,
            ResumeAfterAsync,
        }));
        AddDialog(new InstallAppDialog());
        AddDialog(new LocalAdminDialog());
        AddDialog(new ResetPasswordDialog());
        AddDialog(new ChoicePrompt(nameof(ChoicePrompt)));
    }
    
  4. We can delete the StartAsync method. When a component dialog begins, it automatically starts its initial dialog. In this case, that's the waterfall dialog we defined in the constructor. That also automatically starts at its first step.

  5. We will delete the MessageReceivedAsync and ShowOptions methods, and replace them with the first step of our waterfall. These two methods greeted the user and asked them to choose one of the available options.

    • Here you can see the choice list and the greeting and error messages are provided as options in the call to our choice prompt.
    • We don't have to specify the next method to call in the dialog, as the waterfall will continue to the next step when the choice prompt completes.
    • The choice prompt will loop until it receives valid input or the whole dialog stack is canceled.
    private async Task<DialogTurnResult> PromptForOptionsAsync(
        WaterfallStepContext stepContext,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        // Prompt the user for a response using our choice prompt.
        return await stepContext.PromptAsync(
            nameof(ChoicePrompt),
            new PromptOptions()
            {
                Choices = HelpdeskOptions,
                Prompt = MessageFactory.Text(GreetMessage),
                RetryPrompt = MessageFactory.Text(ErrorMessage)
            },
            cancellationToken);
    }
    
  6. We can replace OnOptionSelected with the second step of our waterfall. We still start a child dialog based on the user's input.

    • The choice prompt returns a FoundChoice value. This shows up in the step context's Result property. The dialog stack treats all return values as objects. If the return value is from one of your dialogs, then you know what type of value the object is. See prompt types for a list what each prompt type returns.
    • Since the choice prompt won't throw an exception, we can remove the try-catch block.
    • We need to add a fall through so that this method always returns an appropriate value. This code should never get hit, but if it does it will allow the dialog to "fail gracefully".
    private async Task<DialogTurnResult> ShowChildDialogAsync(
        WaterfallStepContext stepContext,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        // string optionSelected = await userReply;
        var optionSelected = (stepContext.Result as FoundChoice).Value;
    
        switch (optionSelected)
        {
            case InstallAppOption:
                //context.Call(new InstallAppDialog(), this.ResumeAfterOptionDialog);
                //break;
                return await stepContext.BeginDialogAsync(
                    nameof(InstallAppDialog),
                    cancellationToken);
            case ResetPasswordOption:
                //context.Call(new ResetPasswordDialog(), this.ResumeAfterOptionDialog);
                //break;
                return await stepContext.BeginDialogAsync(
                    nameof(ResetPasswordDialog),
                    cancellationToken);
            case LocalAdminOption:
                //context.Call(new LocalAdminDialog(), this.ResumeAfterOptionDialog);
                //break;
                return await stepContext.BeginDialogAsync(
                    nameof(LocalAdminDialog),
                    cancellationToken);
        }
    
        // We shouldn't get here, but fail gracefully if we do.
        await stepContext.Context.SendActivityAsync(
            "I don't recognize that option.",
            cancellationToken: cancellationToken);
        // Continue through to the next step without starting a child dialog.
        return await stepContext.NextAsync(cancellationToken: cancellationToken);
    }
    
  7. Finally, replace the old ResumeAfterOptionDialog method with the last step of our waterfall.

    • Instead of ending the dialog and returning the ticket number as we did in the original dialog, we're restarting the waterfall by replacing on the stack the original instance with a new instance of itself. We can do this, since the original app always ignored the return value (the ticket number) and restarted the root dialog.
    private async Task<DialogTurnResult> ResumeAfterAsync(
        WaterfallStepContext stepContext,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        try
        {
            //var message = await userReply;
            var message = stepContext.Context.Activity;
    
            var ticketNumber = new Random().Next(0, 20000);
            //await context.PostAsync($"Thank you for using the Helpdesk Bot. Your ticket number is {ticketNumber}.");
            await stepContext.Context.SendActivityAsync(
                $"Thank you for using the Helpdesk Bot. Your ticket number is {ticketNumber}.",
                cancellationToken: cancellationToken);
    
            //context.Done(ticketNumber);
        }
        catch (Exception ex)
        {
            // await context.PostAsync($"Failed with message: {ex.Message}");
            await stepContext.Context.SendActivityAsync(
                $"Failed with message: {ex.Message}",
                cancellationToken: cancellationToken);
    
            // In general resume from task after calling a child dialog is a good place to handle exceptions
            // try catch will capture exceptions from the bot framework awaitable object which is essentially "userReply"
            logger.Error(ex);
        }
    
        // Replace on the stack the current instance of the waterfall with a new instance,
        // and start from the top.
        return await stepContext.ReplaceDialogAsync(
            nameof(WaterfallDialog),
            cancellationToken: cancellationToken);
    }
    

Update the install app dialog

The install app dialog performs a few logical tasks, which we'll set up as a 4-step waterfall dialog. How you factor existing code into waterfall steps is a logical exercise for each dialog. For each step, the original method the code came from is noted.

  1. Asks the user for a search string.
  2. Queries a database for potential matches.
    • If there is one hit, select this and continue.
    • If there are multiple hits, it asks the user to choose one.
    • If there are no hits, the dialog exits.
  3. Asks the user for a machine to install the app on.
  4. Writes the information to a database and sends a confirmation message.

In the Dialogs/InstallAppDialog.cs file:

  1. Update the using statements:

    using ContosoHelpdeskChatBot.Models;
    using Microsoft.Bot.Builder;
    using Microsoft.Bot.Builder.Dialogs;
    using Microsoft.Bot.Builder.Dialogs.Choices;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    
  2. Define a constant for the key we'll use to track collected information.

    // Set up keys for managing collected information.
    private const string InstallInfo = "installInfo";
    
  3. Add a constructor and initialize the component's dialog set.

    public InstallAppDialog()
        : base(nameof(InstallAppDialog))
    {
        // Initialize our dialogs and prompts.
        InitialDialogId = nameof(WaterfallDialog);
        AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[] {
            GetSearchTermAsync,
            ResolveAppNameAsync,
            GetMachineNameAsync,
            SubmitRequestAsync,
        }));
        AddDialog(new TextPrompt(nameof(TextPrompt)));
        AddDialog(new ChoicePrompt(nameof(ChoicePrompt)));
    }
    
  4. We can replace StartAsync with the first step of our waterfall.

    • We have to manage state ourselves, so we'll track the install app object in dialog state.
    • The message asking the user for input becomes an option in the call to the prompt.
    private async Task<DialogTurnResult> GetSearchTermAsync(
        WaterfallStepContext stepContext,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        // Create an object in dialog state in which to track our collected information.
        stepContext.Values[InstallInfo] = new InstallApp();
    
        // Ask for the search term.
        return await stepContext.PromptAsync(
            nameof(TextPrompt),
            new PromptOptions
            {
                Prompt = MessageFactory.Text("Ok let's get started. What is the name of the application? "),
            },
            cancellationToken);
    }
    
  5. We can replace appNameAsync and multipleAppsAsync with the second step of our waterfall.

    • We're getting the prompt result now, instead of just looking at the user's last message.
    • The database query and if statements are organized the same as in appNameAsync. The code in each block of the if statement has been updated to work with v4 dialogs.
      • If we have one hit, we'll update dialog state and continue with the next step.
      • If we have multiple hits, we'll use our choice prompt to ask the user to choose from the list of options. This means we can just delete multipleAppsAsync.
      • If we have no hits, we'll end this dialog and return null to the root dialog.
    private async Task<DialogTurnResult> ResolveAppNameAsync(
        WaterfallStepContext stepContext,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        // Get the result from the text prompt.
        var appname = stepContext.Result as string;
    
        // Query the database for matches.
        var names = await this.GetAppsAsync(appname);
    
        if (names.Count == 1)
        {
            // Get our tracking information from dialog state and add the app name.
            var install = stepContext.Values[InstallInfo] as InstallApp;
            install.AppName = names.First();
    
            return await stepContext.NextAsync();
        }
        else if (names.Count > 1)
        {
            // Ask the user to choose from the list of matches.
            return await stepContext.PromptAsync(
                nameof(ChoicePrompt),
                new PromptOptions
                {
                    Prompt = MessageFactory.Text("I found the following applications. Please choose one:"),
                    Choices = ChoiceFactory.ToChoices(names),
                },
                cancellationToken);
        }
        else
        {
            // If no matches, exit this dialog.
            await stepContext.Context.SendActivityAsync(
                $"Sorry, I did not find any application with the name '{appname}'.",
                cancellationToken: cancellationToken);
    
            return await stepContext.EndDialogAsync(null, cancellationToken);
        }
    }
    
  6. appNameAsync also asked the user for their machine name after it resolved the query. We'll capture that portion of the logic in the next step of the waterfall.

    • Again, in v4 we have to manage state ourselves. The only tricky thing here is that we can get to this step through two different logic branches in the previous step.
    • We'll ask the user for a machine name using the same text prompt as before, just supplying different options this time.
    private async Task<DialogTurnResult> GetMachineNameAsync(
        WaterfallStepContext stepContext,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        // Get the tracking info. If we don't already have an app name,
        // Then we used the choice prompt to get it in the previous step.
        var install = stepContext.Values[InstallInfo] as InstallApp;
        if (install.AppName is null)
        {
            install.AppName = (stepContext.Result as FoundChoice).Value;
        }
    
        // We now need the machine name, so prompt for it.
        return await stepContext.PromptAsync(
            nameof(TextPrompt),
            new PromptOptions
            {
                Prompt = MessageFactory.Text(
                    $"Found {install.AppName}. What is the name of the machine to install application?"),
            },
            cancellationToken);
    }
    
  7. The logic from machineNameAsync is wrapped up in the final step of our waterfall.

    • We retrieve the machine name from the text prompt result and update dialog state.
    • We are removing the call to update the database, as the supporting code is in a different project.
    • Then we're sending the success message to the user and ending the dialog.
    private async Task<DialogTurnResult> SubmitRequestAsync(
        WaterfallStepContext stepContext,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        var install = default(InstallApp);
        if (stepContext.Reason != DialogReason.CancelCalled)
        {
            // Get the tracking info and add the machine name.
            install = stepContext.Values[InstallInfo] as InstallApp;
            install.MachineName = stepContext.Context.Activity.Text;
    
            //TODO: Save to this information to the database.
        }
    
        await stepContext.Context.SendActivityAsync(
            $"Great, your request to install {install.AppName} on {install.MachineName} has been scheduled.",
            cancellationToken: cancellationToken);
    
        return await stepContext.EndDialogAsync(null, cancellationToken);
    }
    
  8. To simulate the database call, we mock up getAppsAsync to query a static list, instead of the database.

    private async Task<List<string>> GetAppsAsync(string Name)
    {
        var names = new List<string>();
    
        // Simulate querying the database for applications that match.
        return (from app in AppMsis
                where app.ToLower().Contains(Name.ToLower())
                select app).ToList();
    }
    
    // Example list of app names in the database.
    private static readonly List<string> AppMsis = new List<string>
    {
        "ĀµTorrent 3.5.0.44178",
        "7-Zip 17.1",
        "Ad-Aware 9.0",
        "Adobe AIR 2.5.1.17730",
        "Adobe Flash Player (IE) 28.0.0.105",
        "Adobe Flash Player (Non-IE) 27.0.0.130",
        "Adobe Reader 11.0.14",
        "Adobe Shockwave Player 12.3.1.201",
        "Advanced SystemCare Personal 11.0.3",
        "Auslogics Disk Defrag 3.6",
        "avast! 4 Home Edition 4.8.1351",
        "AVG Anti-Virus Free Edition 9.0.0.698",
        "Bonjour 3.1.0.1",
        "CCleaner 5.24.5839",
        "Chmod Calculator 20132.4",
        "CyberLink PowerDVD 17.0.2101.62",
        "DAEMON Tools Lite 4.46.1.328",
        "FileZilla Client 3.5",
        "Firefox 57.0",
        "Foxit Reader 4.1.1.805",
        "Google Chrome 66.143.49260",
        "Google Earth 7.3.0.3832",
        "Google Toolbar (IE) 7.5.8231.2252",
        "GSpot 2701.0",
        "Internet Explorer 903235.0",
        "iTunes 12.7.0.166",
        "Java Runtime Environment 6 Update 17",
        "K-Lite Codec Pack 12.1",
        "Malwarebytes Anti-Malware 2.2.1.1043",
        "Media Player Classic 6.4.9.0",
        "Microsoft Silverlight 5.1.50907",
        "Mozilla Thunderbird 57.0",
        "Nero Burning ROM 19.1.1005",
        "OpenOffice.org 3.1.1 Build 9420",
        "Opera 12.18.1873",
        "Paint.NET 4.0.19",
        "Picasa 3.9.141.259",
        "QuickTime 7.79.80.95",
        "RealPlayer SP 12.0.0.319",
        "Revo Uninstaller 1.95",
        "Skype 7.40.151",
        "Spybot - Search & Destroy 1.6.2.46",
        "SpywareBlaster 4.6",
        "TuneUp Utilities 2009 14.0.1000.353",
        "Unlocker 1.9.2",
        "VLC media player 1.1.6",
        "Winamp 5.56 Build 2512",
        "Windows Live Messenger 2009 16.4.3528.331",
        "WinPatrol 2010 31.0.2014",
        "WinRAR 5.0",
    };
    

Update the local admin dialog

In v3, this dialog greeted the user, started the Formflow dialog, and then saved the result off to a database. This translates easily into a two-step waterfall.

  1. Update the using statements. Note that this dialog includes a v3 Formflow dialog. In v4 we can use the community Formflow library.

    using Bot.Builder.Community.Dialogs.FormFlow;
    using ContosoHelpdeskChatBot.Models;
    using Microsoft.Bot.Builder.Dialogs;
    using System.Threading;
    using System.Threading.Tasks;
    
  2. We can remove the instance property for LocalAdmin, as the result will be available in dialog state.

  3. Add a constructor and initialize the component's dialog set. The Formflow dialog is created in the same way. We're just adding it to the dialog set of our component in the constructor.

    public LocalAdminDialog() : base(nameof(LocalAdminDialog))
    {
        InitialDialogId = nameof(WaterfallDialog);
        AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
        {
            BeginFormflowAsync,
            SaveResultAsync,
        }));
        AddDialog(FormDialog.FromForm(BuildLocalAdminForm, FormOptions.PromptInStart));
    }
    
  4. We can replace StartAsync with the first step of our waterfall. We already created the Formflow in the constructor, and the other two statements translate to this. Note that FormBuilder assigns the model's type name as the ID of the generated dialog, which is LocalAdminPrompt for this model.

    private async Task<DialogTurnResult> BeginFormflowAsync(
        WaterfallStepContext stepContext,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        await stepContext.Context.SendActivityAsync("Great I will help you request local machine admin.");
    
        // Begin the Formflow dialog.
        return await stepContext.BeginDialogAsync(
            nameof(LocalAdminPrompt),
            cancellationToken: cancellationToken);
    }
    
  5. We can replace ResumeAfterLocalAdminFormDialog with the second step of our waterfall. We have to get the return value from the step context, instead of from an instance property.

    private async Task<DialogTurnResult> SaveResultAsync(
        WaterfallStepContext stepContext,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        // Get the result from the Formflow dialog when it ends.
        if (stepContext.Reason != DialogReason.CancelCalled)
        {
            var admin = stepContext.Result as LocalAdminPrompt;
    
            //TODO: Save to this information to the database.
        }
    
        return await stepContext.EndDialogAsync(null, cancellationToken);
    }
    
  6. BuildLocalAdminForm remains largely the same, except we don't have the Formflow update the instance property.

    // Nearly the same as before.
    private IForm<LocalAdminPrompt> BuildLocalAdminForm()
    {
        // Here's an example of how validation can be used with FormBuilder.
        return new FormBuilder<LocalAdminPrompt>()
            .Field(nameof(LocalAdminPrompt.MachineName),
            validate: async (state, value) =>
            {
                var result = new ValidateResult { IsValid = true, Value = value };
                //add validation here
    
                //this.admin.MachineName = (string)value;
                return result;
            })
            .Field(nameof(LocalAdminPrompt.AdminDuration),
            validate: async (state, value) =>
            {
                var result = new ValidateResult { IsValid = true, Value = value };
                //add validation here
    
                //this.admin.AdminDuration = Convert.ToInt32((long)value) as int?;
                return result;
            })
            .Build();
    }
    

Update the reset password dialog

In v3, this dialog greeted the user, authorized the user with a pass code, failed out or started the Formflow dialog, and then reset the password. This still translates well into a waterfall.

  1. Update the using statements. Note that this dialog includes a v3 Formflow dialog. In v4 we can use the community Formflow library.

    using Bot.Builder.Community.Dialogs.FormFlow;
    using ContosoHelpdeskChatBot.Models;
    using Microsoft.Bot.Builder.Dialogs;
    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
  2. Add a constructor and initialize the component's dialog set. The Formflow dialog is created in the same way. We're just adding it to the dialog set of our component in the constructor.

    public ResetPasswordDialog()
        : base(nameof(ResetPasswordDialog))
    {
        InitialDialogId = nameof(WaterfallDialog);
        AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
        {
            BeginFormflowAsync,
            ProcessRequestAsync,
        }));
        AddDialog(FormDialog.FromForm(BuildResetPasswordForm, FormOptions.PromptInStart));
    }
    
  3. We can replace StartAsync with the first step of our waterfall. We already created the Formflow in the constructor. Otherwise, we're keeping the same logic, just translating the v3 calls to their v4 equivalents.

    private async Task<DialogTurnResult> BeginFormflowAsync(
        WaterfallStepContext stepContext,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        await stepContext.Context.SendActivityAsync("Alright I will help you create a temp password.");
    
        // Check the passcode and fail out or begin the Formflow dialog.
        if (SendPassCode(stepContext))
        {
            return await stepContext.BeginDialogAsync(
                nameof(ResetPasswordPrompt),
                cancellationToken: cancellationToken);
        }
        else
        {
            //here we can simply fail the current dialog because we have root dialog handling all exceptions
            throw new Exception("Failed to send SMS. Make sure email & phone number has been added to database.");
        }
    }
    
  4. sendPassCode is left mainly as an exercise. The original code is commented out, and the method just returns true. Also, we can remove the email address again, as it wasn't used in the original bot.

    private bool SendPassCode(DialogContext context)
    {
        //bool result = false;
    
        //Recipient Id varies depending on channel
        //refer ChannelAccount class https://docs.botframework.com/en-us/csharp/builder/sdkreference/dd/def/class_microsoft_1_1_bot_1_1_connector_1_1_channel_account.html#a0b89cf01fdd73cbc00a524dce9e2ad1a
        //as well as Activity class https://docs.botframework.com/en-us/csharp/builder/sdkreference/dc/d2f/class_microsoft_1_1_bot_1_1_connector_1_1_activity.html
        //int passcode = new Random().Next(1000, 9999);
        //Int64? smsNumber = 0;
        //string smsMessage = "Your Contoso Pass Code is ";
        //string countryDialPrefix = "+1";
    
        // TODO: save PassCode to database
        //using (var db = new ContosoHelpdeskContext())
        //{
        //    var reset = db.ResetPasswords.Where(r => r.EmailAddress == email).ToList();
        //    if (reset.Count >= 1)
        //    {
        //        reset.First().PassCode = passcode;
        //        smsNumber = reset.First().MobileNumber;
        //        result = true;
        //    }
    
        //    db.SaveChanges();
        //}
    
        // TODO: send passcode to user via SMS.
        //if (result)
        //{
        //    result = Helper.SendSms($"{countryDialPrefix}{smsNumber.ToString()}", $"{smsMessage} {passcode}");
        //}
    
        //return result;
        return true;
    }
    
  5. BuildResetPasswordForm has no changes.

  6. We can replace ResumeAfterResetPasswordFormDialog with the second step of our waterfall, and we'll get the return value from the step context. We've removed the email address that the original dialog didn't do anything with, and we've provided a dummy result instead of querying the database. We're keeping the same logic, just translating the v3 calls to their v4 equivalents.

    private async Task<DialogTurnResult> ProcessRequestAsync(
        WaterfallStepContext stepContext,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        // Get the result from the Formflow dialog when it ends.
        if (stepContext.Reason != DialogReason.CancelCalled)
        {
            var prompt = stepContext.Result as ResetPasswordPrompt;
            int? passcode;
    
            // TODO: Retrieve the passcode from the database.
            passcode = 1111;
    
            if (prompt.PassCode == passcode)
            {
                string temppwd = "TempPwd" + new Random().Next(0, 5000);
                await stepContext.Context.SendActivityAsync(
                    $"Your temp password is {temppwd}",
                    cancellationToken: cancellationToken);
            }
        }
    
        return await stepContext.EndDialogAsync(null, cancellationToken);
    }
    

Copy over and update models as necessary

You can use the same v3 models with the v4 community form flow library.

  1. Create a Models folder in your project.
  2. Copy these files from the v3 project's models directory into your new models directory.
    • InstallApp.cs
    • LocalAdmin.cs
    • LocalAdminPrompt.cs
    • ResetPassword.cs
    • ResetPasswordPrompt.cs

Update using statements

We need to update using statements in the model classes as shown next.

  1. In InstallApps.cs change them to this:

    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    
  2. In LocalAdmin.cs change them to this:

    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    
  3. In LocalAdminPrompt.cs change them to this:

    using Bot.Builder.Community.Dialogs.FormFlow;
    
  4. In ResetPassword.cs change them to this:

    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    

    Also, delete the using statements inside the namespace.

  5. In ResetPasswordPrompt.cs change them to this:

    using Bot.Builder.Community.Dialogs.FormFlow;
    using System;
    

Additional changes

In ResetPassword.cs change the return type of the MobileNumber as follows:

public long? MobileNumber { get; set; }

Final porting steps

To complete the porting process, perform these steps:

  1. Create an AdapterWithErrorHandler class to define an adapter which includes an error handler that can catch exceptions in the middleware or application. The adapter processes and directs incoming activities in through the bot middleware pipeline to your bot's logic and then back out again. Use the following code to create the class:
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Connector.Authentication;
using System;

namespace ContosoHelpdeskChatBot
{
    public class AdapterWithErrorHandler : BotFrameworkHttpAdapter
    {
        private static log4net.ILog logger
            = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        public AdapterWithErrorHandler(
            ICredentialProvider credentialProvider,
            ConversationState conversationState = null)
            : base(credentialProvider)
        {
            OnTurnError = async (turnContext, exception) =>
            {
                // Log any leaked exception from the application.
                logger.Error($"Exception caught : {exception.Message}");

                // Send a catch-all apology to the user.
                await turnContext.SendActivityAsync("Sorry, it looks like something went wrong.");

                if (conversationState != null)
                {
                    try
                    {
                        // Delete the conversationState for the current conversation to prevent the
                        // bot from getting stuck in a error-loop caused by being in a bad state.
                        // ConversationState should be thought of as similar to "cookie-state" in a Web pages.
                        await conversationState.DeleteAsync(turnContext);
                    }
                    catch (Exception e)
                    {
                        logger.Error($"Exception caught on attempting to Delete ConversationState : {e.Message}");
                    }
                }
            };
        }
    }
}
  1. Modify the wwwroot\default.htm page as you see fit.

Run and test your bot in the Emulator

At this point, we should be able to run the bot locally in IIS and attach to it with the Emulator.

  1. Run the bot in IIS.
  2. Start the Emulator and connect to the bot's endpoint (for example, http://localhost:3978/api/messages).
    • If this is the first time you are running the bot then click File > New Bot and follow the instructions on screen. Otherwise, click File > Open Bot to open an existing bot.
    • Double check your port settings in the configuration. For example, if the bot opened in your browser to http://localhost:3979/, then in the Emulator, set the bot's endpoint to http://localhost:3979/api/messages.
  3. All four dialogs should work, and you can set breakpoints in the waterfall steps to check what the dialog context and dialog state is at these points.

Additional resources

v4 conceptual topics:

v4 how-to topics: