1

I am looking at the EchoBot sample and trying to understand it. I see that BotController is mapped to api/messages and HttpPost which in turn invokes Adapter.ProcessAsync. But how does this translate into EchoBot.OnMessageActivityAsync call? I tried to set up a breakpoint and see the call stack but that doesn't help ( see attached screenshot).

enter image description here

I understand BotFrameworkHttpAdapter is invoked via dependency injection. But I don't know how we end up in EchoBot eventually.

Prashant Saraswat
  • 838
  • 1
  • 8
  • 20

1 Answers1

5

To find the answer to this you really have to dive into the source code, so I hope you've got your scuba diving gear because we're going deep.

Step 1

Inside the BotController.cs file the following piece of code is called:

await Adapter.ProcessAsync(Request, Response, Bot);

which calls the ProcessAsync method on the IBotFrameworkHttpAdapter interface.

Step 2

Inside the Startup.cs file we have the following line:

services.AddSingleton<IBotFrameworkHttpAdapter, BotFrameworkHttpAdapter>();

which says that every time we ask for an IBotFrameworkHttpAdapter, provide the same instance of BotFrameworkHttpAdapter - essentially a static variable, you can read more about dependency injection service lifetimes here.

Step 3

Inside the Microsoft.Bot.Builder package we have the implementation for the ProcessAsync method, that can for our purposes be reduced to the following line:

var invokeResponse = await ProcessActivityAsync(authHeader, activity, bot.OnTurnAsync, cancellationToken).ConfigureAwait(false);

which calls ProcessActivityAsync which is another function that lives in this library - the important part here is the bot.OnTurnAsync parameter being passed in.

Step 5

Also inside the Microsoft.Bot.Builder package is the implementation for ProcessActivityAsync:

return await ProcessActivityAsync(claimsIdentity, activity, callback, cancellationToken).ConfigureAwait(false);

which calls an overload of the same method, but before we move on from here, the callback parameter is the bot.OnTurnAsync parameter that was passed through before.

Step 6

The overload of ProcessActivityAsync is also implemented inside the Microsoft.Bot.Builder package, and can be simplified to this line:

await RunPipelineAsync(context, callback, cancellationToken).ConfigureAwait(false);

where callback is bot.OnTurnAsync.

Step 7

Digging deeper still we find the implementation of the RunPipelineAsync method inside of the Microsoft.Bot.Builder package which is were things start to get a bit fuzzy... Theoretically we want to fall through to the else block where the callback (i.e. bot.OnTurnAsync) is called:

// Call any registered Middleware Components looking for ReceiveActivityAsync()
if (turnContext.Activity != null)
{
    // Other code  
}
else
{
    // call back to caller on proactive case
    if (callback != null)
    {
        await callback(turnContext, cancellationToken).ConfigureAwait(false);
    }
}

However, back in Step 6 we also had this line:

using (var context = new TurnContext(this, activity))

where the context is created, and the activity property is initialised. This same context is pass through to the RunPipelineAsync call, which means that we will not fall through to the else block...

But there are the following comment on the RunPipelineAsync method:

/// <param name="callback">A callback method to run at the end of the pipeline.</param>

and inside the remarks section:

...Once control reaches the end of the pipeline, the adapter calls
the <paramref name="callback"/> method...

So I believe is it safe to say that our callback method is being executed which means that we continue by bubbling back up the chain to resolve the function that callback maps to (bot.OnTurnAsync).

Step 8

In BotController we pass in an instance of IBot to the ProcessAsync method, and in Startup we wire up all requests for an IBot to return an instance of EchoBot like so:

// Create the bot as a transient. In this case the ASP Controller is expecting an IBot.
services.AddTransient<IBot, EchoBot>();

The EchoBot implementation inherits from the ActivityHandler class:

public class EchoBot : ActivityHandler

Step 9

The ActivityHandler class provides a default implementation for the OnTurnAsync method which I will simplify to:

switch (turnContext.Activity.Type)
{
   case ActivityTypes.Message:
       return OnMessageActivityAsync(new DelegatingTurnContext<IMessageActivity>(turnContext), cancellationToken);
   // Other cases
}

which the OnMessageActivityAsync method on the same class that has an implementation that returns a completed task, i.e. is a no-op, however it is a virtual method - classes which inherit ActivityHandler can provide their own implementation.

Step 10

A custom implementation for OnMessageActivityAsync is provided inside the EchoBot class:

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    await turnContext.SendActivityAsync(MessageFactory.Text($"Echo: {turnContext.Activity.Text}"), cancellationToken);
}

where the user's input is echoed back to them, and thus our journey ends.


Regarding Step 7, the Microsoft team are pretty active on SO for things tagged with botframework so it might be best to get @mdrichardson or @tdurnford to clarify what happens here.


As an aside in Visual Studio you can debug some library code by enabling the following option:

  • Tools --> Options --> Debugger
  • Uncheck "Enable Just my Code"

Also if you enable navigation to decompiled sources (you will have to accept a legal notification popup) by doing this:

  • Tools --> Options --> Text Editor --> C# --> Advanced
  • Check "Enable navigation to decompiled sources"

You will be able to inspect the source code of external packages within Visual Studio itself.

Matt Stannett
  • 2,700
  • 1
  • 15
  • 36