Convert a .NET v3 bot to a skill

APPLIES TO: SDK v4

This article describes how to convert 3 sample .NET v3 bots to skills and to create a v4 skill consumer that can access these skills. To convert a JavaScript v3 bot to a skill, see how to Convert a JavaScript v3 bot to a skill. To migrate a .NET bot from v3 to v4, see how to Migrate a .NET v3 bot to a .NET Framework v4 bot.

Prerequisites

  • Visual Studio 2019.
  • .NET Core 3.1.
  • .NET Framework 4.6.1 or later.
  • An Azure subscription. If you don't have an Azure subscription, create a free account before you begin.
  • Copies of the v3 .NET sample bots to convert: an echo bot, the PizzaBot, and the SimpleSandwichBot.
  • A copy of the v4 .NET sample skill consumer: SimpleRootBot.

About the bots

In this article, each v3 bot is updated to act as a skill. A v4 skill consumer is included, so that you can test the converted bots as skills.

  • The EchoBot echoes back messages it receives. As a skill, it completes when it receives an "end" or "stop" message. The bot to convert is based on the v3 Bot Builder Echo Bot project template.
  • The PizzaBot walks a user through ordering a pizza. As a skill, it sends the user's order back to the parent when finished.
  • The SimpleSandwichBot walks a user through ordering a sandwich. As a skill, it sends the user's order back to the parent when finished.

Also, a v4 skill consumer, the SimpleRootBot, demonstrates how to consume the skills and allows you to test them.

To use the skill consumer to test the skills, all 4 bots need to be running at the same time. The bots can be tested locally using the Bot Framework Emulator, with each bot using a different local port.

Create Azure resources for the bots

Bot-to-bot authentication requires that each participating bot has a valid app ID and password.

  1. Create a Bot Channels Registration for the bots as needed.
  2. Record the app ID and password for each one.

Conversion process

To convert an existing bot to a skill bot takes just a few steps, as outlined in the next couple sections. For more in-depth information, see about skills.

  • Update the bot's web.config file to set the bot's app ID and password and to add an allowed callers property.
  • Add claims validation. This will restrict access to the skill so that only users or your root bot can access the skill. See the additional information section for more information about default and custom claims validation.
  • Modify the bot's messages controller to handle endOfConversation activities from the root bot.
  • Modify the bot code to return an endOfConversation activity when the skill completes.
  • Whenever the skill completes, if it has conversation state or maintains resources, it should clear its conversation state and release resources.
  • Optionally add a manifest file. Since a skill consumer does not necessarily have access to the skill code, use a skill manifest to describe the activities the skill can receive and generate, its input and output parameters, and the skill's endpoints. The current manifest schema is skill-manifest-2.0.0.json.

Convert the echo bot

  1. Create a new project from the v3 Bot Builder Echo Bot project template, and set it to use port 3979.

    1. Create the project.
    2. Open the project's properties.
    3. Select the Web category, and set the Project Url to http://localhost:3979/.
    4. Save your changes and close the properties tab.
  2. To the configuration file, add the echo bot's app ID and password. Also in app settings, add an EchoBotAllowedCallers property and add the simple root bot's app ID to its value.

    V3EchoBot\Web.config

    <appSettings>
      <!-- update these with your Microsoft App Id and your Microsoft App Password-->
      <add key="MicrosoftAppId" value="YOUR Echo bot's MicrosoftAppId" />
      <add key="MicrosoftAppPassword" value="YOUR Echo bot's MicrosoftAppPassword" />
      <add key="EchoBotAllowedCallers" value="YOUR root bot's MicrosoftAppId" />
    </appSettings>
    
  3. Add a custom claims validator and a supporting authentication configuration class.

    V3EchoBot\Authentication\CustomAllowedCallersClaimsValidator.cs

    This implements custom claims validation and throws an UnauthorizedAccessException if validation fails.

    using Microsoft.Bot.Connector.SkillAuthentication;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;
    using System.Threading.Tasks;
    
    namespace Microsoft.Bot.Sample.EchoBot.Authentication
    {
        /// <summary>
        /// Sample claims validator that loads an allowed list from configuration if present
        /// and checks that requests are coming from allowed parent bots.
        /// </summary>
        public class CustomAllowedCallersClaimsValidator : ClaimsValidator
        {
            private readonly IList<string> _allowedCallers;
    
            public CustomAllowedCallersClaimsValidator(IList<string> allowedCallers)
            {
                // AllowedCallers is the setting in web.config file
                // that consists of the list of parent bot IDs that are allowed to access the skill.
                // To add a new parent bot simply go to the AllowedCallers and add
                // the parent bot's Microsoft app ID to the list.
    
                _allowedCallers = allowedCallers ?? throw new ArgumentNullException(nameof(allowedCallers));
                if (!_allowedCallers.Any())
                {
                    throw new ArgumentNullException(nameof(allowedCallers), "AllowedCallers must contain at least one element of '*' or valid MicrosoftAppId(s).");
                }
            }
    
            /// <summary>
            /// This method is called from JwtTokenValidation.ValidateClaimsAsync
            /// </summary>
            /// <param name="claims"></param>
            public override Task ValidateClaimsAsync(IList<Claim> claims)
            {
                if (claims == null)
                {
                    throw new ArgumentNullException(nameof(claims));
                }
    
                if (!claims.Any())
                {
                    throw new UnauthorizedAccessException("ValidateClaimsAsync.claims parameter must contain at least one element.");
                }
    
                if (SkillValidation.IsSkillClaim(claims))
                {
                    // if _allowedCallers has one item of '*', allow all parent bot calls and do not validate the appid from claims
                    if (_allowedCallers.Count == 1 && _allowedCallers[0] == "*")
                    {
                        return Task.CompletedTask;
                    }
    
                    // Check that the appId claim in the skill request is in the list of skills configured for this bot.
                    var appId = JwtTokenValidation.GetAppIdFromClaims(claims).ToUpperInvariant();
                    if (_allowedCallers.Contains(appId))
                    {
                        return Task.CompletedTask;
                    }
    
                    throw new UnauthorizedAccessException($"Received a request from a bot with an app ID of \"{appId}\". To enable requests from this caller, add the app ID to your configuration file.");
                }
    
                throw new UnauthorizedAccessException($"ValidateClaimsAsync called without a Skill claim in claims.");
            }
        }
    }
    

    V3EchoBot\Authentication\CustomSkillAuthenticationConfiguration.cs

    This loads the allowed callers information from the configuration file and uses the CustomAllowedCallersClaimsValidator for claims validation.

    using Microsoft.Bot.Connector.SkillAuthentication;
    using System.Configuration;
    using System.Linq;
    
    namespace Microsoft.Bot.Sample.EchoBot.Authentication
    {
        public class CustomSkillAuthenticationConfiguration : AuthenticationConfiguration
        {
            private const string AllowedCallersConfigKey = "EchoBotAllowedCallers";
            public CustomSkillAuthenticationConfiguration()
            {
                // Could pull this list from a DB or anywhere.
                var allowedCallers = ConfigurationManager.AppSettings[AllowedCallersConfigKey].Split(',').Select(s => s.Trim().ToUpperInvariant()).ToList();
                ClaimsValidator = new CustomAllowedCallersClaimsValidator(allowedCallers);
            }
        }
    }
    
  4. Update the MessagesController class.

    V3EchoBot\Controllers\MessagesController.cs

    Update the using statements.

    using System.Diagnostics;
    using System.Net;
    using System.Net.Http;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web.Http;
    using Autofac;
    using Microsoft.Bot.Builder.Dialogs;
    using Microsoft.Bot.Builder.Dialogs.Internals;
    using Microsoft.Bot.Connector;
    using Microsoft.Bot.Connector.SkillAuthentication;
    using Microsoft.Bot.Sample.EchoBot.Authentication;
    

    Update the class attribute from BotAuthentication to SkillBotAuthentication. Use the optional AuthenticationConfigurationProviderType parameter to use the custom claims validation provider.

    // Specify which type provides the authentication configuration to allow for validation for skills.
    [SkillBotAuthentication(AuthenticationConfigurationProviderType = typeof(CustomSkillAuthenticationConfiguration))]
    public class MessagesController : ApiController
    

    In the HandleSystemMessage method, add a condition to handle an endOfConversation message. This allows the skill to clear state and release resources when the conversation is ended from the skill consumer.

    if (messageType == ActivityTypes.EndOfConversation)
    {
        Trace.TraceInformation($"EndOfConversation: {message}");
    
        // This Recipient null check is required for PVA manifest validation.
        // PVA will send an EOC activity with null Recipient.
        if (message.Recipient != null)
        {
            // Clear the dialog stack if the root bot has ended the conversation.
            using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message))
            {
                var botData = scope.Resolve<IBotData>();
                await botData.LoadAsync(default(CancellationToken));
    
                var stack = scope.Resolve<IDialogStack>();
                stack.Reset();
    
                await botData.FlushAsync(default(CancellationToken));
            }
        }
    }
    
  5. Modify the bot code to allow the skill to flag that the conversation is complete when it receives an "end" or "stop" message from the user. The skill should also clear state and release resources when it ends the conversation.

    V3EchoBot\Dialogs\RootDialog.cs

    private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
    {
        var activity = await result as Activity;
    
        // Send an `endOfconversation` activity if the user cancels the skill.
        if (activity.Text.ToLower().Contains("end") || activity.Text.ToLower().Contains("stop"))
        {
            await context.PostAsync($"Ending conversation from the skill...");
            var endOfConversation = activity.CreateReply();
            endOfConversation.Type = ActivityTypes.EndOfConversation;
            endOfConversation.Code = EndOfConversationCodes.UserCancelled;
            await context.PostAsync(endOfConversation);
        }
        else
        {
            await context.PostAsync($"Echo (dotnet V3): {activity.Text}");
            await context.PostAsync($"Say 'end' or 'stop' and I'll end the conversation and back to the parent.");
        }
    
        context.Wait(MessageReceivedAsync);
    }
    
  6. Use this manifest for the echo bot. Set the endpoint app ID to the bot's app ID.

    V3EchoBot\wwwroot\echo-bot-manifest.json

    {
      "$schema": "https://raw.githubusercontent.com/microsoft/botframework-sdk/master/schemas/skills/skill-manifest-2.0.0.json",
      "$id": "YourEchoBotHandle",
      "name": "V3 Echo Skill Bot",
      "version": "1.0",
      "description": "This is a sample skill for echoing what the user sent the bot.",
      "publisherName": "Microsoft",
      "privacyUrl": "https://microsoft.com/privacy",
      "copyright": "Copyright (c) Microsoft Corporation. All rights reserved.",
      "license": "",
      "iconUrl": "https://myskill.contoso.com/icon.png",
      "tags": [
        "sample",
        "echo"
      ],
      "endpoints": [
        {
          "name": "YourEchoBotName",
          "protocol": "BotFrameworkV3",
          "description": "Default endpoint for the skill",
          "endpointUrl": "http://localhost:3979/api/messages",
          "msAppId": "Your Echo Bot's MicrosoftAppId"
        }
      ],
      "activities": {
        "EchoDotNetV3": {
          "description": "Echo user responses",
          "type": "message",
          "name": "V3Echo"
        }
      }
    }
    

    For the skill-manifest schema, see skill-manifest-2.0.0.json.

Convert the pizza bot

  1. Open your copy of the PizzaBot project, and set it to use port 3980.

    1. Open the project's properties.
    2. Select the Web category, and set the Project Url to http://localhost:3980/.
    3. Save your changes and close the properties tab.
  2. To the configuration file, add the pizza bot's app ID and password. Also in app settings, add an AllowedCallers property and add the simple root bot's app ID to its value.

    V3PizzaBot\Web.config

    <appSettings>
      <!-- update these with your Microsoft App Id and your Microsoft App Password-->
      <add key="MicrosoftAppId" value="YOUR Pizza bot's MicrosoftAppId" />
      <add key="MicrosoftAppPassword" value="YOUR Pizza bot's MicrosoftAppPassword" />
      <add key="AllowedCallers" value="YOUR root bot's MicrosoftAppId" />
    </appSettings>
    
  3. Add a ConversationHelper class that has helper methods to

    • Send the endOfConversation activity when the skill ends. This can return the order information in the activity's Value property and set the Code property to reflect why the conversation ended.
    • Clear conversation state and release any associated resources.

    V3PizzaBot\ConversationHelper.cs

    using System;
    using System.Collections.Concurrent;
    using System.Configuration;
    using System.Threading;
    using System.Threading.Tasks;
    using Autofac;
    using Microsoft.Bot.Builder.Dialogs;
    using Microsoft.Bot.Builder.Dialogs.Internals;
    using Microsoft.Bot.Connector;
    using Newtonsoft.Json;
    
    namespace Microsoft.Bot.Sample.PizzaBot
    {
        internal static class ConversationHelper
        {
            private static readonly ConcurrentDictionary<string, ConnectorClient> _connectorClientCache = new ConcurrentDictionary<string, ConnectorClient>();
    
            /// <summary>
            /// Helper method that sends an `endOfConversation` activity.
            /// </summary>
            /// <param name="incomingActivity">The incoming user activity for this turn.</param>
            /// <param name="order">Optional. The completed order.</param>
            /// <param name="endOfConversationCode">Optional. The EndOfConversationCode to send to the parent bot.
            /// Defaults to EndOfConversationCodes.CompletedSuccessfully.</param>
            /// <remarks>Sending the `endOfConversation` activity when the conversation completes allows
            /// the bot to be consumed as a skill.</remarks>
            internal static async Task EndConversation(Activity incomingActivity, PizzaOrder order = null, string endOfConversationCode = EndOfConversationCodes.CompletedSuccessfully)
            {
                var connectorClient = _connectorClientCache.GetOrAdd(incomingActivity.ServiceUrl, key =>
                {
                    var appId = ConfigurationManager.AppSettings[MicrosoftAppCredentials.MicrosoftAppIdKey];
                    var appPassword = ConfigurationManager.AppSettings[MicrosoftAppCredentials.MicrosoftAppPasswordKey];
                    return new ConnectorClient(new Uri(incomingActivity.ServiceUrl), appId, appPassword);
                });
    
                // Send End of conversation as reply.
                await connectorClient.Conversations.SendToConversationAsync(incomingActivity.CreateReply("Ending conversation from the skill..."));
                var endOfConversation = incomingActivity.CreateReply();
                if (order != null)
                {
                    endOfConversation.Value = JsonConvert.SerializeObject(order);
                }
                endOfConversation.Type = ActivityTypes.EndOfConversation;
                endOfConversation.Code = endOfConversationCode;
                await connectorClient.Conversations.SendToConversationAsync(endOfConversation);
            }
    
            /// <summary>
            /// Clear the dialog stack and data bags.
            /// </summary>
            /// <param name="activity">The incoming activity to use for scoping the Conversation.Container.</param>
            internal static async Task ClearState(Activity activity)
            {
                // This is required for PVA manifest validation.
                // PVA will send an EOC activity with null Recipient.
                if (activity.Recipient == null)
                    return;
    
                using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, activity))
                {
                    var botData = scope.Resolve<IBotData>();
                    await botData.LoadAsync(default(CancellationToken));
    
                    // Some skills might persist data between invokations.
                    botData.UserData.Clear();
                    botData.ConversationData.Clear();
                    botData.PrivateConversationData.Clear();
    
                    var stack = scope.Resolve<IDialogStack>();
                    stack.Reset();
                    
                    await botData.FlushAsync(default(CancellationToken));
                }
            }
        }
    }
    
  4. Update the MessagesController class.

    V3PizzaBot\Controllers\MessagesController.cs

    Update the using statements.

    using System.Web.Http;
    using System.Threading.Tasks;
    
    using Microsoft.Bot.Connector;
    using Microsoft.Bot.Builder.FormFlow;
    using Microsoft.Bot.Builder.Dialogs;
    using System.Web.Http.Description;
    using System.Net.Http;
    using System.Diagnostics;
    using Microsoft.Bot.Connector.SkillAuthentication;
    using Microsoft.Bot.Builder.Dialogs.Internals;
    using Autofac;
    using System.Threading;
    

    Update the class attribute from BotAuthentication to SkillBotAuthentication. This bot uses the default claims validator.

    [SkillBotAuthentication]
    public class MessagesController : ApiController
    

    In the Post method, modify the message activity condition to allow the user to cancel their ordering process from within the skill. Also, add an endOfConversation activity condition to allow the skill to clear state and release resources when the conversation is ended from the skill consumer.

    case ActivityTypes.Message:
        // Send an `endOfconversation` activity if the user cancels the skill.
        if (activity.Text.ToLower().Contains("end") || activity.Text.ToLower().Contains("stop"))
        {
            await ConversationHelper.ClearState(activity);
            await ConversationHelper.EndConversation(activity, endOfConversationCode: EndOfConversationCodes.UserCancelled);
        }
        else
        {
            await Conversation.SendAsync(activity, MakeRoot);
        }
        break;
    case ActivityTypes.EndOfConversation:
        Trace.TraceInformation($"EndOfConversation: {activity}");
    
        // Clear the dialog stack if the root bot has ended the conversation.
        await ConversationHelper.ClearState(activity);
    
        break;
    
  5. Modify the bot code.

    V3PizzaBot\PizzaOrderDialog.cs

    Update the using statements.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    
    using Microsoft.Bot.Builder.Dialogs;
    using Microsoft.Bot.Builder.FormFlow;
    using Microsoft.Bot.Builder.Luis;
    using Newtonsoft.Json;
    using Microsoft.Bot.Builder.Luis.Models;
    using Microsoft.Bot.Connector;
    

    Add a welcome message. This will help the user know what's going on when the root bot invokes the pizza bot as a skill.

    public override async Task StartAsync(IDialogContext context)
    {
        await context.PostAsync("Welcome to the Pizza Order Bot. Let me know if you would like to order a pizza, or know our store hours.");
        await context.PostAsync("Say 'end' or 'stop' and I'll end the conversation and back to the parent.");
    
        await base.StartAsync(context);
    }
    

    Modify the bot code to allow the skill to flag that the conversation is complete when the user cancels or completes their order. The skill should also clear state and release resources when it ends the conversation.

    private async Task PizzaFormComplete(IDialogContext context, IAwaitable<PizzaOrder> result)
    {
        PizzaOrder order = null;
        try
        {
            order = await result;
        }
        catch (OperationCanceledException)
        {
            await context.PostAsync("You canceled the form!");
    
            // If the user cancels the skill, send an `endOfConversation` activity to the skill consumer.
            await ConversationHelper.EndConversation(context.Activity as Activity, endOfConversationCode: EndOfConversationCodes.UserCancelled);
            return;
        }
    
        if (order != null)
        {
            await context.PostAsync("Your Pizza Order: " + order.ToString());
        }
        else
        {
            await context.PostAsync("Form returned empty response!");
        }
    
        // When the skill completes, send an `endOfConversation` activity and include the finished order.
        await ConversationHelper.EndConversation(context.Activity as Activity, order);
        context.Wait(MessageReceived);
    }
    
  6. Use this manifest for the pizza bot. Set the endpoint app ID to the bot's app ID.

    V3PizzaBot\wwwroot\pizza-bot-manifest.json

    {
      "$schema": "https://raw.githubusercontent.com/microsoft/botframework-sdk/master/schemas/skills/skill-manifest-2.0.0.json",
      "$id": "YourPizzaBotHandle",
      "name": "Pizza Skill Bot",
      "version": "1.0",
      "description": "This is a sample skill for ordering a Pizza.",
      "publisherName": "Microsoft",
      "privacyUrl": "https://microsoft.com/privacy",
      "copyright": "Copyright (c) Microsoft Corporation. All rights reserved.",
      "license": "",
      "iconUrl": "https://myskill.contoso.com/icon.png",
      "tags": [
        "sample",
        "pizza"
      ],
      "endpoints": [
        {
          "name": "YourPizzaBotName",
          "protocol": "BotFrameworkV3",
          "description": "Default endpoint for the skill",
          "endpointUrl": "http://localhost:3980/api/messages",
          "msAppId": "YOUR Pizza Bot's MicrosoftAppId"
        }
      ],
      "activities": {
        "OrderPizza": {
          "description": "Order a Pizza",
          "type": "message",
          "name": "OrderPizza"
        }
      }
    }
    

    For the skill-manifest schema, see skill-manifest-2.0.0.json.

Convert the sandwich bot

  1. Open your copy of the SimpleSandwichBot project, and set it to use port 3981.

    1. Open the project's properties.
    2. Select the Web category, and set the Project Url to http://localhost:3981/.
    3. Save your changes and close the properties tab.
  2. To the configuration file, add the pizza bot's app ID and password. Also in app settings, add an AllowedCallers property and add the simple root bot's app ID to its value.

    V3SimpleSandwichBot\Web.config

    <appSettings>
      <!-- update these with your Microsoft App Id and your Microsoft App Password-->
      <add key="MicrosoftAppId" value="YOUR Sandwich bot's MicrosoftAppId" />
      <add key="MicrosoftAppPassword" value="YOUR Sandwich bot's MicrosoftAppPassword" />
      <add key="AllowedCallers" value="YOUR root bot's MicrosoftAppId" />
    </appSettings>
    
  3. Add a ConversationHelper class that has helper methods to

    • Send the endOfConversation activity when the skill ends. This can return the order information in the activity's Value property and set the Code property to reflect why the conversation ended.
    • Clear conversation state and release any associated resources.

    V3SimpleSandwichBot\ConversationHelper.cs

    using System;
    using System.Collections.Concurrent;
    using System.Configuration;
    using System.Threading;
    using System.Threading.Tasks;
    using Autofac;
    using Microsoft.Bot.Builder.Dialogs;
    using Microsoft.Bot.Builder.Dialogs.Internals;
    using Microsoft.Bot.Connector;
    using Newtonsoft.Json;
    
    namespace Microsoft.Bot.Sample.SimpleSandwichBot
    {
        internal static class ConversationHelper
        {
            private static readonly ConcurrentDictionary<string, ConnectorClient> _connectorClientCache = new ConcurrentDictionary<string, ConnectorClient>();
    
            /// <summary>
            /// Helper method that sends an `endOfConversation` activity.
            /// </summary>
            /// <param name="incomingActivity">The incoming user activity for this turn.</param>
            /// <param name="order">Optional. The completed order.</param>
            /// <param name="endOfConversationCode">Optional. The EndOfConversationCode to send to the parent bot.
            /// Defaults to EndOfConversationCodes.CompletedSuccessfully.</param>
            /// <remarks>Sending the `endOfConversation` activity when the conversation completes allows
            /// the bot to be consumed as a skill.</remarks>
            internal static async Task EndConversation(Activity incomingActivity, SandwichOrder order = null, string endOfConversationCode = EndOfConversationCodes.CompletedSuccessfully)
            {
                var connectorClient = _connectorClientCache.GetOrAdd(incomingActivity.ServiceUrl, key =>
                {
                    var appId = ConfigurationManager.AppSettings[MicrosoftAppCredentials.MicrosoftAppIdKey];
                    var appPassword = ConfigurationManager.AppSettings[MicrosoftAppCredentials.MicrosoftAppPasswordKey];
                    return new ConnectorClient(new Uri(incomingActivity.ServiceUrl), appId, appPassword);
                });
    
                // Send End of conversation as reply.
                await connectorClient.Conversations.SendToConversationAsync(incomingActivity.CreateReply("Ending conversation from the skill..."));
                var endOfConversation = incomingActivity.CreateReply();
                if (order != null)
                {
                    endOfConversation.Value = JsonConvert.SerializeObject(order);
                }
                endOfConversation.Type = ActivityTypes.EndOfConversation;
                endOfConversation.Code = endOfConversationCode;
                await connectorClient.Conversations.SendToConversationAsync(endOfConversation);
            }
    
            /// <summary>
            /// Clear the dialog stack and data bags.
            /// </summary>
            /// <param name="activity">The incoming activity to use for scoping the Conversation.Container.</param>
            internal static async Task ClearState(Activity activity)
            {
                // This Recipient null check is required for PVA manifest validation.
                // PVA will send an EOC activity with null Recipient.
                if (activity.Recipient == null)
                    return;
    
                using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, activity))
                {
                    var botData = scope.Resolve<IBotData>();
                    await botData.LoadAsync(default(CancellationToken));
    
                    // Some skills might persist data between invokations.
                    botData.UserData.Clear();
                    botData.ConversationData.Clear();
                    botData.PrivateConversationData.Clear();
    
                    var stack = scope.Resolve<IDialogStack>();
                    stack.Reset();
    
                    await botData.FlushAsync(default(CancellationToken));
                }
            }
        }
    }
    
  4. Update the MessagesController class.

    V3SimpleSandwichBot\Controllers\MessagesController.cs

    Update the using statements.

    using System.Threading.Tasks;
    using System.Web.Http;
    
    using Microsoft.Bot.Connector;
    using Microsoft.Bot.Builder.Dialogs;
    using Microsoft.Bot.Builder.FormFlow;
    using System.Net.Http;
    using System.Web.Http.Description;
    using System.Diagnostics;
    using Microsoft.Bot.Connector.SkillAuthentication;
    using Newtonsoft.Json;
    using Microsoft.Bot.Builder.Dialogs.Internals;
    using Autofac;
    using System.Threading;
    

    Update the class attribute from BotAuthentication to SkillBotAuthentication. This bot uses the default claims validator.

    [SkillBotAuthentication]
    public class MessagesController : ApiController
    

    In the Post method, modify the message activity condition to allow the user to cancel their ordering process from within the skill. Also, add an endOfConversation activity condition to allow the skill to clear state and release resources when the conversation is ended from the skill consumer.

    case ActivityTypes.Message:
        if (activity.Text.ToLower().Contains("end") || activity.Text.ToLower().Contains("stop"))
        {
            await ConversationHelper.ClearState(activity);
            await ConversationHelper.EndConversation(activity, endOfConversationCode: EndOfConversationCodes.UserCancelled);
        }
        else
        {
            await Conversation.SendAsync(activity, MakeRootDialog);
        }
    
        break;
    case ActivityTypes.EndOfConversation:
        Trace.TraceInformation($"EndOfConversation: {activity}");
    
        // Clear the dialog stack if the root bot has ended the conversation.
        await ConversationHelper.ClearState(activity);
    
        break;
    
  5. Modify the sandwich form.

    V3SimpleSandwichBot\Sandwich.cs

    Update the using statements.

    using Microsoft.Bot.Builder.FormFlow;
    using Microsoft.Bot.Connector;
    using System;
    using System.Collections.Generic;
    using System.Runtime.Remoting.Messaging;
    

    Modify the form's BuildForm method to allow the skill to flag that the conversation is complete.

    public static IForm<SandwichOrder> BuildForm()
    {
        // When the skill completes (OnCompletion), send an `endOfConversation` activity and include the finished order.
        return new FormBuilder<SandwichOrder>()
                .Message("Welcome to the simple sandwich order bot! Say 'end' or 'stop' and I'll end the conversation and back to the parent.")
                .OnCompletion((context, order) => ConversationHelper.EndConversation(context.Activity as Activity, order))
                .Build();
    }
    
  6. Use this manifest for the sandwich bot. Set the endpoint app ID to the bot's app ID.

    V3SimpleSandwichBot\wwwroot\sandwich-bot-manifest.json

    {
      "$schema": "https://raw.githubusercontent.com/microsoft/botframework-sdk/master/schemas/skills/skill-manifest-2.0.0.json",
      "$id": "YourSandwichBotHandle",
      "name": "Sandwich Skill Bot",
      "version": "1.0",
      "description": "This is a sample skill for ordering a sandwich.",
      "publisherName": "Microsoft",
      "privacyUrl": "https://microsoft.com/privacy",
      "copyright": "Copyright (c) Microsoft Corporation. All rights reserved.",
      "license": "",
      "iconUrl": "https://myskill.contoso.com/icon.png",
      "tags": [
        "sample",
        "pizza"
      ],
      "endpoints": [
        {
          "name": "YourSandwichBotName",
          "protocol": "BotFrameworkV3",
          "description": "Default endpoint for the skill",
          "endpointUrl": "http://localhost:3981/api/messages",
          "msAppId": "YOUR Sandwich Bots MicrosoftAppId"
        }
      ],
      "activities": {
        "OrderSandwich": {
          "description": "Order a sandwich",
          "type": "message",
          "name": "OrderSandwich"
        }
      }
    }
    

    For the skill-manifest schema, see skill-manifest-2.0.0.json.

Create the v4 root bot

The simple root bot consumes the 3 skills and lets you verify that the conversion steps worked as planned. This bot will run locally on port 3978.

  1. To the configuration file, add the root bot's app ID and password. For each of the v3 skills, add the skill's app ID.

    V4SimpleRootBot\appsettings.json

    {
      "MicrosoftAppId": "YOUR Root Skill Host bot's MicrosoftAppId",
      "MicrosoftAppPassword": "YOUR Root Skill Host bot's MicrosoftAppPassword",
      "SkillHostEndpoint": "http://localhost:3978/api/skills",
      "BotFrameworkSkills": [
        {
          "Id": "Echo",
          "AppId": "YOUR Echo bot's MicrosoftAppId",
          "SkillEndpoint": "http://localhost:3979/api/messages"
        },
        {
          "Id": "Pizza",
          "AppId": "YOUR Pizza bot's MicrosoftAppId",
          "SkillEndpoint": "http://localhost:3980/api/messages"
        },
        {
          "Id": "Sandwich",
          "AppId": "YOUR Sandwich bot's MicrosoftAppId",
          "SkillEndpoint": "http://localhost:3981/api/messages"
        }
      ]
    }
    

Test the root bot

Download and install the latest Bot Framework Emulator.

  1. Build and run all four bots locally on your machine.
  2. Use the Emulator to connect to the root bot.
  3. Test the skills and skill consumer.

Additional information

Bot-to-bot authentication

The root and skill communicate over HTTP. The framework uses bearer tokens and bot application IDs to verify the identity of each bot. It uses an authentication configuration object to validate the authentication header on incoming requests. You can add a claims validator to the authentication configuration. The claims are evaluated after the authentication header. Your validation code should throw an error or exception to reject the request.

The default claims validator reads the AllowedCallers application setting from the bot's configuration file. This setting should contain a comma separated list of the application IDs of the bots that are allowed to call the skill, or "*" to allow all bots to call the skill.

To implement a custom claims validator, implement classes that derive from AuthenticationConfiguration and ClaimsValidator and then reference the derived authentication configuration in the SkillBotAuthentication attribute. Steps 3 and 4 of the convert the echo bot section has example claims validation classes.