0

I'm designing a C# bot using the Microsoft Bot Framework. I'd like the bot to be easily extensible, allowing developers to add dialogs/responses while making code changes in as few places as possible. From my own experiments and looking at the examples in Microsoft's BotBuilder repo, it seems like adding a new dialog/response requires adding code in two places - a dispatcher in either MessagesController or RootDialog, plus the actual code implementing the dialog. Is there any way to structure the code so that extensions only require adding code in one place?

Example code:

From MessagesController.cs:

[...]
/// <summary>
/// POST: api/Messages
/// Receive a message from a user and reply to it
/// </summary>
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
    if (activity.Type == ActivityTypes.Message)
    {
        await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
     }
     else
     {
         HandleSystemMessage(activity);
     }
     var response = Request.CreateResponse(HttpStatusCode.OK);
     return response;
 }
[...]

This method is the main entrypoint to the bot; any messages it receives trigger a POST call to this endpoint. In this code, that just triggers RootDialog creation and execution, but it could be written to trigger a different dialog depending on the details of the message.

From RootDialog.cs:

[...]
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
    var activity = await result as Activity;

    if (activity.Text == "command-one")
    {
        await context.PostAsync("Received command one.");
    } 
    else if (activity.Text == "command-two")
    {
        someOtherObject.doSomething();
        await context.PostAsync("Received command two.");
    }
    else
    {
        // calculate something for us to return
        int length = (activity.Text ?? string.Empty).Length;

        // return our reply to the user
        await context.PostAsync($"You sent {activity.Text} which was {length} characters");
    }

    context.Wait(MessageReceivedAsync);
}
[...]

This method does different things depending on the message text. In the command-one case, it just displays a simple response. Extensions along these lines just require adding code to this method, which is fine. In the command-two case, it calls the method of some other class, which may perform some arbitrary other activity. Adding an extension like this requires another class as well as adding a dispatcher to RootDialog. To restate my question, is there a way to avoid having to add the dispatcher in RootDialog, perhaps by adding some sort of annotation to methods in other classes?

  • Write an adapter? en.wikipedia.org/wiki/Adapter_pattern – Robert Harvey May 16 '17 at 18:52
  • 2
    You can't unless you provide constraints on what sort of extensions you imagine making. – whatsisname May 16 '17 at 20:53
  • 1
    Added some example code; hopefully that clarifies things. – DylanSp May 17 '17 at 17:12
  • A simple Map<string, Command> linking a command name to a function / delegate / object to call on that command would solve the problem. In MessageReceivedAsync, get the command by name, execute it if it's present, report an unknown command if it's absent. – 9000 May 17 '17 at 17:22
  • 1
    @9000 (and Jules) Could one of you make that into an answer? – DylanSp May 17 '17 at 18:08
1

The idea: map command names to actual commands, execute by name.

I'm abusing System.Window.Input.ICommand here to show the pattern; a custom or another existing interface can be used.

Dictionary<string, ICommand> COMMANDS = ...;

// ...

if (COMMANDS.ContainsKey(command_name)) {
  COMMANDS[command_name].Invoke(...); // Pass any context a command might need.
} else {
  // Handle 'Command not found' situation.
}

I'd build the COMMANDS by interpreting a config file, (likely at startup time unless you dynamically load assemblies for commands implementations, then you could do it periodically). This allows to selectively enable / disable and rename commands without rebuilding the code.

| improve this answer | |
  • Thanks for the answer. I'll test this out; if I can get it working, I'll accept your answer. – DylanSp May 18 '17 at 19:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.