Skip to content

Microsoft Bot Framework in ConnectSoft Templates

Purpose & Overview

Microsoft Bot Framework in ConnectSoft Templates enables building conversational AI bots that can interact with users across multiple channels (Teams, Slack, web chat, etc.). ConnectSoft Templates provide a complete bot infrastructure with dialogs, state management, command routing, adaptive cards, OAuth authentication, and error handling.

The Microsoft Bot Framework integration provides:

  • Multi-Channel Support: Deploy bots to Teams, Slack, web chat, and more
  • Conversational AI: Natural language interactions with users
  • Dialog Management: Structured conversation flows with waterfall dialogs
  • State Management: Conversation and user state persistence
  • Command Routing: Slash command handling (e.g., /help, /status)
  • Adaptive Cards: Rich, interactive UI cards
  • OAuth Integration: Secure user authentication
  • Error Handling: Graceful error recovery and user feedback
  • Telemetry: Application Insights integration for monitoring

Bot Framework Philosophy

Microsoft Bot Framework enables building intelligent, conversational experiences that feel natural to users. ConnectSoft Templates integrate bot capabilities seamlessly with the application architecture, enabling teams to build chatbots, virtual assistants, and conversational interfaces that integrate with business logic, domain services, and messaging systems.

Architecture Overview

Bot Framework Integration Stack

HTTP Request (/api/messages)
ApplicationBotController (ASP.NET Core Controller)
IBotFrameworkHttpAdapter (AdapterWithErrorHandler)
    ├── Authentication (BotFrameworkAuthentication)
    ├── Middleware Pipeline
    │   ├── TelemetryInitializerMiddleware (if Application Insights enabled)
    │   ├── CommandRoutingMiddleware (slash commands)
    │   ├── ShowTypingMiddleware (UX enhancement)
    │   └── SetSpeakMiddleware (speech synthesis)
    └── Error Handling (OnTurnError)
IBot (ApplicationBot<ApplicationDialog>)
    ├── TeamsActivityHandler (base class)
    ├── Activity Handlers
    │   ├── OnMessageActivityAsync (messages)
    │   ├── OnMembersAddedAsync (conversation updates)
    │   ├── OnTokenResponseEventAsync (OAuth)
    │   └── OnTeamsSigninVerifyStateAsync (Teams sign-in)
    ├── State Management
    │   ├── ConversationState (conversation-scoped state)
    │   └── UserState (user-scoped state)
    └── Dialog System
        └── ApplicationDialog (ComponentDialog)
            ├── WaterfallDialog (conversation flow)
            ├── OAuthPrompt (authentication)
            └── ConfirmPrompt (user confirmation)

Key Integration Points

Layer Component Responsibility
ApplicationModel MicrosoftBotBuilderExtensions Service registration and configuration
BotModel ApplicationBotController HTTP endpoint for bot messages
BotModel AdapterWithErrorHandler Bot adapter with error handling
BotModel ApplicationBot Bot activity handler
BotModel ApplicationDialog Dialog implementation
BotModel CommandRoutingMiddleware Slash command routing
BotModel AdaptiveCardsHelper Adaptive card creation

Service Registration

Bot Framework Setup

Bot Framework is registered via extension method:

// Program.cs or Startup.cs
#if UseMicrosoftBotBuilder
services.AddApplicationMicrosoftBotBuilder();
#endif

Service Registration Details

The AddApplicationMicrosoftBotBuilder() extension method configures all bot services:

// MicrosoftBotBuilderExtensions.cs
internal static IServiceCollection AddApplicationMicrosoftBotBuilder(
    this IServiceCollection services)
{
    // Bot Framework Authentication
    services.AddSingleton<BotFrameworkAuthentication, ConfigurationBotFrameworkAuthentication>();

    // Bot Adapter with error handling
    services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();

    // Storage (MemoryStorage for testing, can be replaced with BlobsStorage for production)
    services.AddSingleton<IStorage, MemoryStorage>();

    // Command routing middleware
    services.AddSingleton<CommandRoutingMiddleware>();

    // User and Conversation state
    services.AddSingleton<UserState>();
    services.AddSingleton<ConversationState>();

    // Main dialog
    services.AddSingleton<ApplicationDialog>();

    // Bot instance (transient per request)
    services.AddTransient<IBot, ApplicationBot<ApplicationDialog>>();

    return services;
}

Service Lifetimes: - Singleton: Adapter, storage, state, dialogs (shared across requests) - Transient: Bot instance (created per turn/request)

Configuration

Bot Framework Configuration

Bot Framework requires configuration in appsettings.json:

{
  "MicrosoftAppId": "af76b7e1-6e83-4359-97e3-6bfe9066d684",
  "MicrosoftAppPassword": "ZBa8Q~naHMmbqV0OUudc1NI6~D49M27_Ffn~~cub",
  "ConnectionName": "GenericWordpressConnection",
  "OAuthUrl": "https://europe.api.botframework.com",
  "ToChannelFromBotOAuthScope": "https://api.botframework.com"
}

Configuration Options:

Option Type Description
MicrosoftAppId string Bot application identifier (Azure Bot Service)
MicrosoftAppPassword string Bot application password (secret)
ConnectionName string OAuth2 connection name (Azure Bot Service)
OAuthUrl string OAuth URL for bot authentication
ToChannelFromBotOAuthScope string OAuth scope for channel authentication

Security

Never commit MicrosoftAppPassword to source control. Use environment variables, Azure Key Vault, or secure configuration management in production.

Bot Controller

HTTP Endpoint

The bot controller handles incoming HTTP requests from Bot Framework:

// ApplicationBotController.cs
[Route("api/messages")]
[ApiController]
public class ApplicationBotController : ControllerBase
{
    private readonly IBotFrameworkHttpAdapter adapter;
    private readonly IBot bot;

    public MicroserviceTemplateBotController(
        IBotFrameworkHttpAdapter adapter,
        IBot bot)
    {
        this.adapter = adapter;
        this.bot = bot;
    }

    [HttpPost]
    [HttpGet]
    public async Task PostAsync(CancellationToken cancellationToken)
    {
        // Delegate to adapter for processing
        await this.adapter.ProcessAsync(
            this.Request, 
            this.Response, 
            this.bot, 
            cancellationToken);
    }
}

Endpoint: POST /api/messages (also supports GET for health checks)

The adapter handles: - Authentication and authorization - Activity deserialization - Routing to bot handlers - Response serialization

Bot Adapter

AdapterWithErrorHandler

The custom adapter extends CloudAdapter with error handling and middleware:

// AdapterWithErrorHandler.cs
public class AdapterWithErrorHandler : CloudAdapter
{
    private readonly ConversationState conversationState;
    private readonly IBotTelemetryClient telemetryClient;

    public AdapterWithErrorHandler(
        BotFrameworkAuthentication auth,
        ILogger<IBotFrameworkHttpAdapter> logger,
        TelemetryInitializerMiddleware telemetryInitializerMiddleware,
        IBotTelemetryClient telemetryClient,
        CommandRoutingMiddleware commandRoutingMiddleware,
        ConversationState conversationState)
        : base(auth, logger)
    {
        this.conversationState = conversationState;
        this.telemetryClient = telemetryClient;

        // Error handler
        this.OnTurnError = this.OnTurnErrorProcessor;

        // Middleware pipeline (order matters)
        this.Use(telemetryInitializerMiddleware);  // Telemetry first
        this.Use(commandRoutingMiddleware);         // Command routing
        this.Use(new ShowTypingMiddleware());       // UX enhancement
        this.Use(new SetSpeakMiddleware("en-US-JennyNeural", fallbackToTextForSpeak: true));
    }

    private async Task OnTurnErrorProcessor(ITurnContext turnContext, Exception exception)
    {
        // Log error
        this.Logger.LogError(exception, "[OnTurnError] unhandled error");

        // Track in telemetry
        this.telemetryClient.TrackException(exception);

        // Send error message to user
        await this.SendErrorMessageAsync(turnContext, exception);

        // Clear conversation state to prevent error loops
        if (this.conversationState != null)
        {
            try
            {
                await this.conversationState.DeleteAsync(turnContext);
            }
            catch (Exception e)
            {
                this.Logger.LogError(e, "Failed to delete conversation state");
            }
        }
    }

    private async Task SendErrorMessageAsync(ITurnContext turnContext, Exception exception)
    {
        // Send adaptive card with error message
        var errorCard = AdaptiveCardsHelper.CreateAdaptiveCardAttachment(
            "Application.BotModel.Cards.GenericErrorCard.json");
        var reply = MessageFactory.Attachment(errorCard);
        await turnContext.SendActivityAsync(reply);

        // Trace activity for debugging
        await turnContext.TraceActivityAsync(
            "OnTurnError Trace", 
            exception.ToString(), 
            "https://www.botframework.com/schemas/error", 
            "TurnError");
    }
}

Key Features: - Error Handling: Catches exceptions and sends user-friendly error messages - State Cleanup: Deletes conversation state on errors to prevent loops - Telemetry: Tracks errors in Application Insights - Middleware Pipeline: Extensible middleware for cross-cutting concerns

Bot Activity Handler

ApplicationBot

The bot extends TeamsActivityHandler to handle different activity types:

// ApplicationBot.cs
public class ApplicationBot<TDialogType> : TeamsActivityHandler
    where TDialogType : Dialog
{
    private readonly ConversationState conversationState;
    private readonly UserState userState;
    private readonly IStatePropertyAccessor<DialogState> dialogStateAccessor;
    private readonly TDialogType dialog;

    public ApplicationBot(
        ILogger<ApplicationBot<TDialogType>> logger,
        ConversationState conversationState,
        UserState userState,
        TDialogType dialog)
    {
        this.conversationState = conversationState;
        this.userState = userState;
        this.dialog = dialog;
        this.dialogStateAccessor = conversationState.CreateProperty<DialogState>(nameof(DialogState));
    }

    public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
    {
        await base.OnTurnAsync(turnContext, cancellationToken);

        // Save state after each turn
        await this.conversationState.SaveChangesAsync(turnContext, force: false, cancellationToken);
        await this.userState.SaveChangesAsync(turnContext, force: false, cancellationToken);
    }

    protected override async Task OnMessageActivityAsync(
        ITurnContext<IMessageActivity> turnContext, 
        CancellationToken cancellationToken)
    {
        // Run dialog for message activities
        await this.dialog.RunAsync(turnContext, this.dialogStateAccessor, cancellationToken);
    }

    protected override async Task OnMembersAddedAsync(
        IList<ChannelAccount> membersAdded, 
        ITurnContext<IConversationUpdateActivity> turnContext, 
        CancellationToken cancellationToken)
    {
        // Welcome new members
        foreach (var member in membersAdded)
        {
            if (!string.Equals(member.Id, turnContext.Activity.Recipient.Id, StringComparison.Ordinal))
            {
                var welcomeCard = AdaptiveCardsHelper.CreateAdaptiveCardAttachment(
                    "Application.BotModel.Cards.WelcomeCard.json");
                var response = MessageFactory.Attachment(welcomeCard, ssml: "Welcome to Application Bot");
                await turnContext.SendActivityAsync(response, cancellationToken);
            }
        }

        // Start dialog
        await this.dialog.RunAsync(turnContext, this.dialogStateAccessor, cancellationToken);
    }

    protected override async Task OnTokenResponseEventAsync(
        ITurnContext<IEventActivity> turnContext, 
        CancellationToken cancellationToken)
    {
        // Handle OAuth token response
        await this.dialog.RunAsync(turnContext, this.dialogStateAccessor, cancellationToken);
    }

    protected override async Task OnTeamsSigninVerifyStateAsync(
        ITurnContext<IInvokeActivity> turnContext, 
        CancellationToken cancellationToken)
    {
        // Handle Teams sign-in verification
        await this.dialog.RunAsync(turnContext, this.dialogStateAccessor, cancellationToken);
    }
}

Activity Handlers: - OnMessageActivityAsync: Handles text messages - OnMembersAddedAsync: Welcomes new conversation members - OnTokenResponseEventAsync: Handles OAuth token responses - OnTeamsSigninVerifyStateAsync: Handles Teams sign-in verification

Dialogs

Dialog System

Dialogs manage conversation flows using a waterfall pattern:

// ApplicationDialog.cs
public class ApplicationDialog : ComponentDialog
{
    private readonly ILogger<ApplicationDialog> logger;

    public ApplicationDialog(ILogger<ApplicationDialog> logger)
        : base(nameof(ApplicationDialog))
    {
        this.logger = logger;

        // Add prompts
        this.AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
        this.AddDialog(new OAuthPrompt(
            nameof(OAuthPrompt),
            new OAuthPromptSettings
            {
                ConnectionName = "GenericWordpressConnection",
                Text = "Please Sign In",
                Title = "Sign In",
                Timeout = 300000, // 5 minutes
            }));

        // Define waterfall steps
        var waterfallSteps = new WaterfallStep[]
        {
            this.WelcomeStepAsync,
            this.LoginStepAsync,
        };

        this.AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));

        // Set initial dialog
        this.InitialDialogId = nameof(WaterfallDialog);
    }

    private async Task<DialogTurnResult> WelcomeStepAsync(
        WaterfallStepContext stepContext, 
        CancellationToken cancellationToken)
    {
        // Begin OAuth prompt
        return await stepContext.BeginDialogAsync(nameof(OAuthPrompt), null, cancellationToken);
    }

    private async Task<DialogTurnResult> LoginStepAsync(
        WaterfallStepContext stepContext, 
        CancellationToken cancellationToken)
    {
        var tokenResponse = (TokenResponse)stepContext.Result;
        if (tokenResponse != null)
        {
            await stepContext.Context.SendActivityAsync(
                MessageFactory.Text("You are now logged in."), 
                cancellationToken);

            return await stepContext.PromptAsync(
                nameof(ConfirmPrompt), 
                new PromptOptions 
                { 
                    Prompt = MessageFactory.Text("Would you like to view your token?") 
                }, 
                cancellationToken);
        }

        await stepContext.Context.SendActivityAsync(
            MessageFactory.Text("Login was not successful please try again."), 
            cancellationToken);

        return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
    }
}

Dialog Types: - ComponentDialog: Base class for dialogs with sub-dialogs - WaterfallDialog: Sequential conversation steps - OAuthPrompt: User authentication - ConfirmPrompt: Yes/No confirmation - TextPrompt: Text input - NumberPrompt: Numeric input - ChoicePrompt: Multiple choice selection

State Management

Conversation State

Conversation state is scoped to a conversation and persists across turns:

// Conversation state accessor
var conversationStateAccessor = conversationState.CreateProperty<MyConversationData>("MyConversationData");

// Read state
var conversationData = await conversationStateAccessor.GetAsync(turnContext, () => new MyConversationData());

// Update state
conversationData.LastMessage = turnContext.Activity.Text;
await conversationStateAccessor.SetAsync(turnContext, conversationData);

// Save state
await conversationState.SaveChangesAsync(turnContext);

User State

User state is scoped to a user and persists across conversations:

// User state accessor
var userStateAccessor = userState.CreateProperty<MyUserData>("MyUserData");

// Read state
var userData = await userStateAccessor.GetAsync(turnContext, () => new MyUserData());

// Update state
userData.Preferences = preferences;
await userStateAccessor.SetAsync(turnContext, userData);

// Save state
await userState.SaveChangesAsync(turnContext);

Storage Providers

MemoryStorage (default, for testing):

services.AddSingleton<IStorage, MemoryStorage>();

BlobsStorage (production, Azure):

services.AddSingleton<IStorage>(sp =>
{
    var connectionString = configuration.GetConnectionString("AzureStorage");
    return new BlobsStorage(connectionString, "bot-state");
});

CosmosDBStorage (production, Azure Cosmos DB):

services.AddSingleton<IStorage>(sp =>
{
    var connectionString = configuration.GetConnectionString("CosmosDB");
    return new CosmosDbPartitionedStorage(new CosmosDbPartitionedStorageOptions
    {
        CosmosDbEndpoint = new Uri(endpoint),
        AuthKey = key,
        DatabaseId = "bot-db",
        ContainerId = "bot-state"
    });
});

Command Routing

Command Routing Middleware

The CommandRoutingMiddleware intercepts slash commands (e.g., /help, /status):

// CommandRoutingMiddleware.cs
public sealed class CommandRoutingMiddleware : IMiddleware
{
    private readonly IReadOnlyDictionary<string, ICommandHandler> handlersByName;

    public CommandRoutingMiddleware(IEnumerable<ICommandHandler> handlers)
    {
        // Create case-insensitive lookup
        this.handlersByName = handlers
            .GroupBy(h => h.Name, StringComparer.OrdinalIgnoreCase)
            .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
    }

    public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default)
    {
        // Reset per-turn flags
        turnContext.TurnState[CommandRoutingMiddleware.HandledFlagKey] = false;

        // Parse command
        var text = turnContext.Activity?.Text;
        if (CommandParser.TryParse(text, out var commandName, out var args) && commandName is not null)
        {
            if (this.handlersByName.TryGetValue(commandName, out var handler))
            {
                var handled = await handler.HandleAsync(turnContext, args, cancellationToken);
                if (handled)
                {
                    turnContext.TurnState[CommandRoutingMiddleware.HandledFlagKey] = true;
                    turnContext.TurnState[CommandRoutingMiddleware.HandledCommandNameKey] = commandName;
                }
            }
        }

        // Continue pipeline
        await next(cancellationToken);
    }
}

Command Handler Interface

// ICommandHandler.cs
public interface ICommandHandler
{
    string Name { get; }
    Task<bool> HandleAsync(ITurnContext turnContext, string[] args, CancellationToken cancellationToken);
}

Command Handler Example

public class HelpCommandHandler : ICommandHandler
{
    public string Name => "help";

    public async Task<bool> HandleAsync(ITurnContext turnContext, string[] args, CancellationToken cancellationToken)
    {
        var helpText = "Available commands:\n" +
                      "/help - Show this help message\n" +
                      "/status - Show bot status";

        await turnContext.SendActivityAsync(MessageFactory.Text(helpText), cancellationToken);
        return true; // Command handled
    }
}

Command Parser

The CommandParser parses slash commands:

// CommandParser.cs
public static class CommandParser
{
    public static bool TryParse(string? text, out string? commandName, out string[] args)
    {
        // Parse "/command arg1 arg2" format
        // Returns command name and arguments array
    }
}

Supported Formats: - /help - Simple command - /status detailed - Command with arguments - /set key value - Command with multiple arguments

Adaptive Cards

Adaptive Cards Helper

The AdaptiveCardsHelper creates adaptive card attachments:

// AdaptiveCardsHelper.cs
internal static class AdaptiveCardsHelper
{
    internal static Attachment CreateAdaptiveCardAttachment(
        string cardResourcePath, 
        Dictionary<string, string>? data = null)
    {
        // Load card JSON from embedded resource
        using var stream = typeof(ApplicationDialog).Assembly.GetManifestResourceStream(cardResourcePath);
        using var reader = new StreamReader(stream);
        var adaptiveCard = reader.ReadToEnd();

        // Replace placeholders with data
        if (data != null)
        {
            foreach (var key in data.Keys)
            {
                var safeValue = JsonConvert.ToString(data[key] ?? string.Empty).Trim('"');
                adaptiveCard = adaptiveCard.Replace($"{{{{{key}}}}}", safeValue, StringComparison.Ordinal);
            }
        }

        return new Attachment()
        {
            ContentType = "application/vnd.microsoft.card.adaptive",
            Content = JsonConvert.DeserializeObject(adaptiveCard),
        };
    }
}

Using Adaptive Cards

// Create adaptive card
var card = AdaptiveCardsHelper.CreateAdaptiveCardAttachment(
    "Application.BotModel.Cards.WelcomeCard.json",
    new Dictionary<string, string>
    {
        ["UserName"] = turnContext.Activity.From.Name,
        ["BotName"] = "My Bot"
    });

// Send card
var response = MessageFactory.Attachment(card);
await turnContext.SendActivityAsync(response, cancellationToken);

OAuth Authentication

OAuth Prompt

OAuth prompts enable user authentication:

// Add OAuth prompt
this.AddDialog(new OAuthPrompt(
    nameof(OAuthPrompt),
    new OAuthPromptSettings
    {
        ConnectionName = "GenericWordpressConnection", // Azure Bot Service OAuth connection
        Text = "Please Sign In",
        Title = "Sign In",
        Timeout = 300000, // 5 minutes
    }));

// Begin OAuth prompt
var result = await stepContext.BeginDialogAsync(nameof(OAuthPrompt), null, cancellationToken);

// Get token response
var tokenResponse = (TokenResponse)stepContext.Result;
if (tokenResponse != null)
{
    // User is authenticated
    var token = tokenResponse.Token;
    // Use token for API calls
}

OAuth Configuration

OAuth connections are configured in Azure Bot Service: 1. Create OAuth connection in Azure Portal 2. Configure connection name in appsettings.json 3. Users will be prompted to sign in when OAuth prompt is shown

Telemetry

Application Insights Integration

Bot Framework integrates with Application Insights:

// Service registration
#if UseApplicationInsights
services.AddSingleton<IBotTelemetryClient, BotTelemetryClient>();
services.AddSingleton<ITelemetryInitializer, TelemetryBotIdInitializer>();
services.AddSingleton<TelemetryInitializerMiddleware>();
services.AddSingleton<TelemetryLoggerMiddleware>();
#endif

Telemetry Events: - Conversation Tracking: Track bot conversations and user interactions - Intent Recognition: Track LUIS/QnA Maker intents and entities - Dialog Flow: Track dialog state transitions - Custom Events: Track custom bot events

Example Telemetry:

public class MyBot : ActivityHandler
{
    private readonly IBotTelemetryClient telemetryClient;

    protected override async Task OnMessageActivityAsync(
        ITurnContext<IMessageActivity> turnContext, 
        CancellationToken cancellationToken)
    {
        // Track message received
        this.telemetryClient.TrackEvent("MessageReceived", new Dictionary<string, string>
        {
            ["UserId"] = turnContext.Activity.From.Id,
            ["Channel"] = turnContext.Activity.ChannelId,
            ["Message"] = turnContext.Activity.Text
        });

        // Bot logic
        await turnContext.SendActivityAsync("Hello!");
    }
}

See Application Insights for more details on telemetry integration.

Testing

Unit Testing

Test bot logic using TestAdapter:

[TestMethod]
public async Task OnMessageActivityRunsDialog()
{
    var conversationState = new ConversationState(new MemoryStorage());
    var userState = new UserState(new MemoryStorage());
    var dialog = new ApplicationDialog(logger);
    var bot = new ApplicationBot<ApplicationDialog>(
        logger, conversationState, userState, dialog);

    var adapter = new TestAdapter(TestAdapter.CreateConversation("test"));

    await new TestFlow(adapter, async (turn, ct) => await bot.OnTurnAsync(turn, ct))
        .Send("hi")
        .AssertReply("Expected response")
        .StartTestAsync();
}

Integration Testing

Test bot endpoints using WebApplicationFactory:

[TestMethod]
public async Task BotEndpoint_ShouldProcessMessage()
{
    var factory = new WebApplicationFactory<Program>();
    var client = factory.CreateClient();

    var activity = new Activity
    {
        Type = ActivityTypes.Message,
        Text = "Hello",
        From = new ChannelAccount { Id = "user1" },
        Conversation = new ConversationAccount { Id = "conv1" }
    };

    var response = await client.PostAsJsonAsync("/api/messages", activity);
    response.EnsureSuccessStatusCode();
}

BDD Testing

Test bot conversations using Reqnroll:

[Given(@"Conversation with Microsoft Template Bot")]
public void GivenConversationWithMicrosoftTemplateBot()
{
    var memoryStorage = new MemoryStorage();
    var conversationState = new ConversationState(memoryStorage);
    var userState = new UserState(memoryStorage);
    var dialog = new ApplicationDialog(logger);
    this.bot = new ApplicationBot<ApplicationDialog>(
        logger, conversationState, userState, dialog);
}

[When(@"User sends a message")]
public async Task WhenUserSendsAMessage()
{
    var testAdapter = new TestAdapter();
    var testFlow = new TestFlow(testAdapter, this.bot);
    await testFlow.Send("Hi").StartTestAsync();
}

Best Practices

Do's

  1. Use Dialogs for Conversation Flows

    // ✅ GOOD - Structured dialog flow
    var waterfallSteps = new WaterfallStep[]
    {
        WelcomeStepAsync,
        CollectInfoStepAsync,
        ConfirmStepAsync
    };
    

  2. Save State Explicitly

    // ✅ GOOD - Save state after modifications
    await conversationState.SaveChangesAsync(turnContext);
    

  3. Handle Errors Gracefully

    // ✅ GOOD - Send user-friendly error messages
    await turnContext.SendActivityAsync("Sorry, something went wrong. Please try again.");
    

  4. Use Adaptive Cards for Rich UI

    // ✅ GOOD - Rich, interactive cards
    var card = AdaptiveCardsHelper.CreateAdaptiveCardAttachment("WelcomeCard.json");
    await turnContext.SendActivityAsync(MessageFactory.Attachment(card));
    

  5. Track Telemetry

    // ✅ GOOD - Track bot interactions
    this.telemetryClient.TrackEvent("UserCommand", new Dictionary<string, string>
    {
        ["Command"] = commandName,
        ["UserId"] = turnContext.Activity.From.Id
    });
    

Don'ts

  1. Don't Block on Long Operations

    // ❌ BAD - Blocks turn
    var result = await LongRunningOperation(); // May timeout
    
    // ✅ GOOD - Use background jobs or async patterns
    await turnContext.SendActivityAsync("Processing your request...");
    // Process in background
    

  2. Don't Store Sensitive Data in State

    // ❌ BAD - Sensitive data in state
    conversationData.Password = password;
    
    // ✅ GOOD - Use secure storage or OAuth tokens
    conversationData.OAuthToken = tokenResponse.Token;
    

  3. Don't Ignore Errors

    // ❌ BAD - Errors swallowed
    try { await operation(); } catch { }
    
    // ✅ GOOD - Log and handle errors
    try { await operation(); }
    catch (Exception ex)
    {
        this.logger.LogError(ex, "Operation failed");
        await turnContext.SendActivityAsync("Error occurred");
    }
    

  4. Don't Hardcode Messages

    // ❌ BAD - Hardcoded messages
    await turnContext.SendActivityAsync("Hello");
    
    // ✅ GOOD - Use localization
    var localizedMessage = localizer["Greeting"];
    await turnContext.SendActivityAsync(localizedMessage);
    

Troubleshooting

Issue: Bot Not Responding

Symptoms: Bot doesn't respond to messages.

Solutions: 1. Verify MicrosoftAppId and MicrosoftAppPassword are correct 2. Check bot endpoint is accessible (/api/messages) 3. Verify authentication is working (check logs) 4. Ensure dialog is being started in OnMessageActivityAsync

Issue: State Not Persisting

Symptoms: State is lost between turns.

Solutions: 1. Verify SaveChangesAsync is called after state modifications 2. Check storage provider is configured correctly 3. For production, use persistent storage (BlobsStorage, CosmosDBStorage) 4. Verify state accessors are created correctly

Issue: OAuth Not Working

Symptoms: OAuth prompt doesn't appear or authentication fails.

Solutions: 1. Verify ConnectionName matches Azure Bot Service OAuth connection 2. Check OAuth connection is configured in Azure Portal 3. Verify OAuthUrl and ToChannelFromBotOAuthScope are correct 4. Check logs for authentication errors

Issue: Commands Not Working

Symptoms: Slash commands aren't being handled.

Solutions: 1. Verify CommandRoutingMiddleware is registered in adapter 2. Check command handlers are registered in DI 3. Verify command name matches handler name (case-insensitive) 4. Check HandledFlagKey in turn state to see if command was handled

Issue: Adaptive Cards Not Rendering

Symptoms: Adaptive cards appear as JSON or don't render.

Solutions: 1. Verify card JSON is valid Adaptive Card schema 2. Check ContentType is "application/vnd.microsoft.card.adaptive" 3. Verify card is sent as attachment via MessageFactory.Attachment 4. Check channel supports Adaptive Cards (some channels have limitations)

Summary

Microsoft Bot Framework in ConnectSoft Templates provides:

  • Multi-Channel Support: Deploy to Teams, Slack, web chat, and more
  • Conversational AI: Natural language interactions with users
  • Dialog Management: Structured conversation flows
  • State Management: Conversation and user state persistence
  • Command Routing: Slash command handling
  • Adaptive Cards: Rich, interactive UI cards
  • OAuth Integration: Secure user authentication
  • Error Handling: Graceful error recovery
  • Telemetry: Application Insights integration
  • Testing: Unit, integration, and BDD testing support

By following these patterns, teams can:

  • Build Intelligent Bots: Create conversational AI experiences
  • Integrate with Business Logic: Connect bots to domain services
  • Scale Conversations: Handle multiple users and channels
  • Monitor Performance: Track bot interactions and errors
  • Test Thoroughly: Write comprehensive tests for bot logic

The Microsoft Bot Framework integration ensures that conversational AI capabilities are built in a clean, maintainable, and testable way, enabling teams to create engaging user experiences that integrate seamlessly with the application architecture across all ConnectSoft templates and solutions.