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):
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¶
-
Use Dialogs for Conversation Flows
-
Save State Explicitly
-
Handle Errors Gracefully
-
Use Adaptive Cards for Rich UI
-
Track Telemetry
Don'ts¶
-
Don't Block on Long Operations
-
Don't Store Sensitive Data in State
-
Don't Ignore Errors
-
Don't Hardcode Messages
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.