I'm always excited to take on new projects and collaborate with innovative minds.
A deep dive into a production-ready, fully-tested flight-booking bot built with Microsoft Bot Framework v4, LUIS, and .NET 6 covering dialogs, intent recognition, interruption handling, and a comprehensive xUnit test suite.
Building a conversational AI demo is relatively straightforward; shipping one that handles real-world usage is significantly harder. A typical bot demo functions smoothly in an emulator but often degrades when a user strays from the happy path or inputs unstructured data.
Creating a resilient bot requires managing natural language understanding, maintaining state across multi-turn conversations, handling mid-flow interruptions, and validating inputs—all backed by an automated test suite.
This article walks through a reference implementation of a flight-booking chatbot built on .NET 6 using the Microsoft Bot Framework SDK v4. It incorporates LUIS for natural language understanding, waterfall dialogs for state management, Adaptive Cards for rich UI elements, and a comprehensive xUnit + Moq test suite for deterministic testing.
The bot is designed to process natural language inputs and guide users through a flight-booking flow.
When provided with a complete request:
User: Book a flight from Paris to Berlin on March 22, 2020
Bot: Please confirm, I have you traveling to: Berlin from: Paris on: 2020-03-22. Is this correct?
User: yes
Bot: I have you booked to Berlin from Paris on March 22, 2020
Bot: What else can I do for you?
Beyond the standard booking flow, the system handles real-world interaction patterns:
Slot Filling: If information is missing (e.g., "I want to fly to Seattle"), the bot prompts sequentially for the origin, destination, and date until all required fields are satisfied.
Global Interruptions: Commands such as help or cancel are intercepted at any point in the conversation without losing or corrupting state.
Input Validation: Dates are validated to ensure they are complete and unambiguous before triggering a confirmation step.
Domain Guardrails: Inputs are validated against supported entities (e.g., verifying supported origin/destination cities).
User Onboarding: Incoming users receive a welcome Adaptive Card upon connecting to the conversation thread.
| Layer | Component |
|---|---|
| Runtime / Language | C# on .NET 6 |
| Bot Framework | Microsoft Bot Framework SDK v4 (v4.16.0) |
| Language Understanding | LUIS (Microsoft.Bot.Builder.AI.Luis) |
| Messaging UI | Adaptive Cards (welcomeCard.json) |
| State Storage | In-Memory (MemoryStorage) — configurable for Azure Blob/Cosmos DB |
| Testing Suite | xUnit, Moq, Microsoft.Bot.Builder.Testing |
The solution builds upon the Microsoft Core Bot template, adding extended test coverage, modular dialog structures, and mock fixtures:
CoreBotWithTests1.sln
├── CoreBotWithTests1/
│ ├── Bots/ # Activity handlers (welcome + dialog execution)
│ ├── Cards/ # welcomeCard.json (Adaptive Card definition)
│ ├── CognitiveModels/ # LUIS model definitions & C# generated classes
│ ├── Controllers/ # ASP.NET endpoint for message ingestion
│ └── Dialogs/ # Conversational workflow and dialog logic
└── CoreBotWithTests1.Tests/ # Unit tests (xUnit + Moq)
Conversational flows are managed within the Dialogs directory, split across four core components:
MainDialog: The top-level orchestrator. It evaluates recognized intents and routes execution to specific child dialogs.
BookingDialog: Manages the sequential collection of destination, origin, and travel date via a waterfall dialog.
DateResolverDialog: Validates and resolves date inputs, reprompting if an entry is ambiguous.
CancelAndHelpDialog: A base class that intercepts top-level commands like help or cancel across all active child steps.
Each step in a waterfall dialog represents an asynchronous function that can prompt the user or pass processed data to the next step:
public async Task<DialogTurnResult> DestinationStepAsync(
WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var bookingDetails = (BookingDetails)stepContext.Options;
if (bookingDetails.Destination == null)
{
return await stepContext.PromptAsync(
nameof(TextPrompt),
new PromptOptions { Prompt = MessageFactory.Text("Where would you like to travel to?") },
cancellationToken);
}
return await stepContext.NextAsync(bookingDetails.Destination, cancellationToken);
}
Intent extraction and entity recognition are handled via a pre-built model (CognitiveModels/FlightBooking.json), defining:
Intents: BookFlight, GetWeather, None
Entities: From, To, FromDate, ToDate
The FlightBookingRecognizer wraps the API call, mapping extracted entities directly to internal booking models.
If LUIS credentials are not present, the bot detects the missing configuration and gracefully falls back to a manual step-by-step prompt flow.
{
"LuisAppId": "your-luis-app-id",
"LuisAPIKey": "your-luis-key",
"LuisAPIHostName": "your-region.api.cognitive.microsoft.com"
}
Note: LUIS is being phased out in favor of Azure AI Language (Conversational Language Understanding / CLU). For new implementations, CLU is recommended; however, the dialog structure and entity mapping used here transition directly to the modern service.
Testing conversational flows often presents challenges due to external cloud dependencies. This repository isolates dialog logic using DialogTestClient from the Microsoft.Bot.Builder.Testing package, enabling local execution of multi-turn interactions.
var sut = new BookingDialog();
var testClient = new DialogTestClient(Channels.Test, sut);
var reply = await testClient.SendActivityAsync<IMessageActivity>("hi");
Assert.Equal("Where would you like to travel to?", reply.Text);
reply = await testClient.SendActivityAsync<IMessageActivity>("Seattle");
Assert.Equal("Where are you traveling from?", reply.Text);
reply = await testClient.SendActivityAsync<IMessageActivity>("New York");
Assert.Equal("When would you like to travel?", reply.Text);
| Test File | Verification Scope |
|---|---|
BookingDialogTests | Multi-turn booking execution paths and cancellation flows. |
DateResolverDialogTests | Date parsing, verification, and reprompting scenarios. |
CancelAndHelpDialogTests | Interruption handling for help, ?, cancel, and quit. |
MainDialogTests | Intent routing, unsupported entity handling, and offline fallback. |
DialogAndWelcomeBotTests | Verification of Adaptive Card rendering using TestAdapter. |
DialogBotTests | Turn execution and state persistence. |
BotControllerTests | HTTP POST endpoint delegation to the adapter. |
The suite uses mock recognizers (SimpleMockFactory) backed by static JSON fixtures in TestData/. This ensures dotnet test runs deterministically without network calls, cloud deployment, or active API keys.
Restore dependencies:
dotnet restore
Execute tests:
dotnet test
Run the host API:
cd CoreBotWithTests1/CoreBotWithTests1
dotnet run
Test via Emulator:
Open the Bot Framework Emulator and connect to http://localhost:3978/api/messages.
Implement Graceful Fallbacks: System logic should handle missing external dependencies (like unconfigured NLU services) without crashing or blocking basic execution.
Treat Interruptions as First-Class Scenarios: Users will frequently enter commands out of sequence. Base dialogs like CancelAndHelpDialog maintain conversational integrity across deep stacks.
Test the Turn State: Unit tests should evaluate the actual output text and state transitions turn-by-turn using tools like DialogTestClient.
Decouple Cloud Services from Local Testing: Replacing external APIs with static JSON fixtures keeps CI/CD pipelines fast and deterministic.
Your email address will not be published. Required fields are marked *