Sending channel specific messages on Microsoft Bot Framework

A fundamental reason to use a framework to write a chat bot is so you don't need to do it all yourself, specially, do it all over again if you want your bot running on a new channel.

However, once in a while, you might find yourself needing to do something very specific to a particular channel, maybe you want to get that receipt template rendered just the way you want to, or maybe you want to use a native functionality of Skype - whatever it might be, here's how you could do it, when using Bot Framework.

The basics

On the IMessageActivity you will find the ChannelData property. Whatever you set in there is serialized, as is, directly to the channel, without transformation. Depending on the channel, BotFramework will populate the basics of the message, so your channel data should correspond to a specific section of the actual message to the channel. So make sure to check their documentation.

For instance, on Facebook, a request would look like (you can check their APIs documentation):

{
  "recipient": {
    "id": "USER_ID"
  },
  "message": {
    "text": "hello, world!"
  }
}

However the contents of ChannelData will be stuffed in the message member. You can control the recipient through the Recipient property in the IMessageActivity.

The poor's man implementation

Given that we can control the message that goes to the channel with the ChannelData property, the poor's man channel-specific implementation could look like this:

var response = context.MakeMessage();

if (message.ChannelId == "facebook")  
{
     response.ChannelData = " { ... facebook specific payload ... }"
}
else if (message.ChannelId == "slack")  
{
     response.ChannelData = " { ... slack specific payload ... }"
}
...

Functionally, this is fine. However the if/else throughout the code will make it harder to maintain and add to the code base

Keeping channel specific logic separate

When you send a message through the IDialogContext it will be processed by several implementations of IBotToUser. One of those is the MapToChannelData_BotToUser class. It allows you to implement the IMessageActivityMapper interface that is called to before any message is sent to the user, giving you a chance to do some additional work there. The interface looks like this:

public interface IMessageActivityMapper  
{
    IMessageActivity Map(IMessageActivity message);
}

And you can register your implementation with the framework like this:

var currentContainier = Microsoft.Bot.Builder.Dialogs.Conversation.Container;

var builder = new ContainerBuilder();

builder  
     // replace the type ChannelSpecificMapper here with your class that implements IMessageActivityMapper
    .RegisterType<ChannelSpecificMapper>()
    .As<IMessageActivityMapper>()
    .InstancePerLifetimeScope();

builder.Update(currentContainier);  

Add that in your service initialization (Startup.cs for ASP.NET Core).
What this does is register our implementation of IMessageActivityMapper, in the code above called ChannelSpecificMapper, with BotBuilder.

Now whenever a message is sent to the user, the method Map in our class ChannelSpecificMapper will be called. So what can we do with it? Here's a proposed implementation:

public class ChannelSpecificMapper : IMessageActivityMapper  
{
  /// <summary>
  /// A map between channelID (key) and the transformer implementation (value).
  /// </summary>
  /// <remarks>ChannelID is defined by the bot framework. Check link below for details.</remarks>
  /// <seealso cref="https://docs.botframework.com/en-us/csharp/builder/sdkreference/channels.html#customfacebookmessages"/>
  private static readonly Dictionary<string, IMessageActivityMapper> Transformers =
    new Dictionary<string, IMessageActivityMapper>(StringComparer.OrdinalIgnoreCase)
  {
    { "facebook", new FacebookMessageMapper() }
    /* ... add more channels here ... */
  };

  private static readonly IMessageActivityMapper DefaultMapper = new DefaultMessageMapper();

  /// <summary>
  /// Maps a message to its channel specific format.
  /// </summary>
  /// <param name="message">The message.</param>
  /// <returns>The transformed message.</returns>
  public IMessageActivity Map(IMessageActivity message)
  {
    var customMessage = message.ChannelData;

    if (customMessage != null)
    {
        IMessageActivityMapper transformer;
        if (!Transformers.TryGetValue(message.ChannelId, out transformer))
        {
            transformer = DefaultMapper;
        }

        message = transformer.Map(message);
    }

    return message;
  }
}

What it does is simply look up an implementation of IMessageActivityMapper registered in the Transformers dictionary by channel id and call Map there.

So now, you can implement a mapper for each channel, register it in that dictionary and handle the channel specific logic logic separately for each channel, instead of having that spread across your code base.

Here is a sample implementation of a mapper for Facebook:

public class FacebookMessageMapper : IMessageActivityMapper  
{
  public IMessageActivity Map(IMessageActivity message)
  {
    if (message.ChannelData is MyCustomReceipt)
    {
      var customReceipt = (MyCustomReceipt)message.ChannelData;
      message.ChannelData = magicCodeThatGeneratesFacebookSpecificPayload(customReceipt);
    }
    else { /* handle other payload types */ }

    return message;
  }
}

And here is how you would create your message, without having to add if/elses everywhere:

var response = context.MakeMessage();  
response.ChannelData = new MyCustomReceipt()  
{
  Receipient = "Andre",
  PaymentMethod = "Card",    
  /* ... */
}
context.Post(response);  

So on Post, the framework would call our ChannelSpecificMapper implementation that would route this message to our FacebookMessageMapper that would then notice the MyCustomReceipt on the channel data and replace it with the channel specific payload.