CHAPTER 5
Things are about to getting really interesting, challenging, and, to some extent, complex.
We’re going to get into what bot building is all about, such as handling conversational states and flow, asynchronous requests, and interaction with external services and performing validations.
The bot’s code base will be too large to fit into this chapter, so I highly recommend that you download the full Visual Studio solution with all the code and follow along with what will be explained here.
In this chapter, we’ll cover the most important parts of the code that are directly related to core bot competencies such as conversional state, flow, validations, and even the bot’s brain, MessagesController.cs.
The bot also has quite a bit of code that deals with the QPX Express API and how it interacts with Azure Storage (tables and blobs) to store flight requests that can be followed for price changes.
Some code files that are part of the bot won’t be covered in this chapter, including Consts.cs, QpxExpressApiHelper.cs, QpxExpressCore.cs, TableStorage.cs, and TableStorageCore.cs. However, if you download the project and check the code, you’ll see comments that will help you navigate and understand what they do. I highly recommend that you do this—it will also present a bit of a challenge for you, as it will force you to see how all the pieces of the puzzle fit together.
The code is nicely structured so that it is readable and easy to follow, and the full VS solution can be found here.
By now you have downloaded the VS solution with all of the bot’s code. Open the Solution Explorer and look for the Web.config file. There are some important settings that need to be edited.
Code Listing 5.0: Web.config Settings to be Edited
<appSettings> <!-- update these with yours --> <add key="BotId" value="AirfareAlertBot" /> <add key="MicrosoftAppId" value="" /> <add key="MicrosoftAppPassword" value="" /> <add key="QpxApiKey" value="" /> <add key="FollowUpInterval" value="60000" /> <add key="StorageConnectionString" value="" /> </appSettings> |
Let’s go over these settings quickly.
The BotId is the bot’s unique identifier. This is how the bot will be known to the Bot Connector when we register the bot on the developer portal and when we publish it on Azure App Services. I recommend you stay consistent and use the same name on all services.
The MicrosoftAppId and MicrosoftAppPassword keys are provided by the Bot Developer Portal—refer to Chapter 3 for more details.
The QpxApiKey represents the QPX Express API key. The FollowUpInterval represents the number of milliseconds that the bot will wait before attempting to check if flight prices have changed.
For testing, you can use a value of 60,000 milliseconds (one minute) for FollowUpInterval, which means that the bot will query the QPX Express API every minute, which might be too frequent going forward (as you get only 50 free API calls per day).
I recommend that you use a higher value, such as 3,600,000 milliseconds (one hour), after the testing phase. This will probably be better, as it will lower your QPX Express API consumption rate and make your free tier last longer.
The StorageConnectionString represents the connection string to Azure Storage. More details about how to set up and work with Azure Storage can be found in my other e-book, Customer Success for C# Developers Succinctly—feel free to check this out.
To run the project locally on your machine, you don’t need to specify the MicrosoftAppId or MicrosoftAppPassword keys—these are only required when registering it on the developer portal.
The other values are required to run the bot locally.
When we create our VS project using the Bot Application template, some code is created for us behind the scenes. Part of that autogenerated code is contained within the WebApiConfig.cs file, which is found under the App_Start folder of the VS project.
In essence, a Bot Application template is nothing more than a slightly enhanced WebApi project.
All airports throughout the world have a corresponding IATA code, which is what airlines and travel companies use when they refer to commercial flights. These codes are printed on suitcase tags and boarding passes. They are also used when making a reservation or booking a flight, so we need to be clear that all commercial flight information is therefore bound to IATA codes for both origin and destination.
By doing a simple search on the Internet for JSON IATA codes, I was able to find this airports.json file that contains a wealth of information about airports, their codes, and other interesting details such as time zone, longitude, latitude, altitude, city, and country.
By parsing this file, our bot should be able to quickly identify any origin or destination and easily retrieve the corresponding IATA codes that are required when calling the QPX Express API.
Let’s create a class that implements IDisposable with a method responsible for parsing this JSON data file and populating a Dictionary object that will be accessible as a public property containing the list of airports.
Before we write this code, let’s install the RestSharp library from NuGet (see Figure 2.7) that will be required in order to retrieve the JSON data.
The Json.NET library will be required to parse and deserialize the JSON response. This library has already been installed as a dependency when we created the project, which means there is no need to install it from NuGet.
Once RestSharp has been installed, let’s add a new C# class file to our VS project called FlightData.cs under the Controllers folder. Here’s the code.
Code Listing 5.1: FlightData.cs
using System.Collections.Generic; using RestSharp; using RestSharp.Deserializers; using System; using System.Threading.Tasks; using Google.Apis.QPXExpress.v1.Data; namespace AirfareAlertBot.Controllers { // Used in order to store worldwide airport data. public class Airport { public string Name { get; set; } public string City { get; set; } public string Country { get; set; } public string Iata { get; set; } public string Icao { get; set; } public float Latitude { get; set; } public float Longitude { get; set; } public float Altitude { get; set; } public string Timezone { get; set; } public string Dst { get; set; } }
// Used in order to store a flight request. public class FlightDetails { public string OriginIata { get; set; } public string DestinationIata { get; set; } public string OutboundDate { get; set; } public string InboundDate { get; set; } public string NumPassengers { get; set; } public string NumResults { get; set; } public string Follow { get; set; } public string Direct { get; set; } public string UserId { get; set; } public int Posi { get; set; } } // Responsible for processing a flight request. public class ProcessFlight : IDisposable { protected bool disposed; public Dictionary<string, Airport> Airports { get; set; } public FlightDetails FlightDetails { get; set; } public ProcessFlight() { FlightDetails = new FlightDetails() { OriginIata = string.Empty, DestinationIata = string.Empty, OutboundDate = string.Empty, InboundDate = string.Empty, NumPassengers = string.Empty, NumResults = string.Empty }; Airports = GetAirports(); } // Gets a list of all airports worldwide. protected Dictionary<string, Airport> GetAirports() { string res = string.Empty; RestClient client = new RestClient(StrConsts.cStrIataCodesBase); RestRequest request = new RestRequest(StrConsts.cStrIataCodePath, Method.GET); request.RequestFormat = DataFormat.Json; IRestResponse response = client.Execute(request); JsonDeserializer deserial = new JsonDeserializer(); return deserial. Deserialize<Dictionary<string, Airport>>(response); } // Checks if a string is an IATA code. public bool IsIataCode(string input, ref List<string> tmpList) { bool res = false; foreach (KeyValuePair<string, Airport> p in Airports) { if (p.Key.ToLower() == input.ToLower()) { tmpList.Add(p.Key); res = true; break; } } return res; } // City that corresponds to an IATA code. public string GetAirportCity(string code) { string res = string.Empty; foreach (KeyValuePair<string, Airport> p in Airports) { if (p.Key.ToLower() == code.ToLower()) { res = p.Value.City; break; } } return res; } // List of IATA codes. public List<string> GetCodesList() { List<string> codes = new List<string>(); foreach (KeyValuePair<string, Airport> p in Airports) { codes.Add(p.Key); } return codes; } protected bool HasAirportParamValue(string v, string p) { return (p != string.Empty && v.ToLower() == p.ToLower()) ? true : false; } // Searches for IATA codes based on the city or airport name. public string[] GetIataCodes(string name, string city, string country) { List<string> codes = new List<string>(); foreach (KeyValuePair<string, Airport> p in Airports) { if (city != string.Empty) { if ((HasAirportParamValue(p.Value.Name, name) || HasAirportParamValue(p.Value.City, city)) || HasAirportParamValue(p.Value.Country, country)) { codes.Add(p.Key + "|" + p.Value.Name); } } else if (name != string.Empty) { if ((HasAirportParamValue(p.Value.City, city) || HasAirportParamValue(p.Value.Name, name)) || HasAirportParamValue(p.Value.Country, country)) { codes.Add(p.Key + "|" + p.Value.Name); } } } return codes.ToArray(); } // Footer of a flight request. protected static string SetOutputFooter(string guid) { return Environment.NewLine + Environment.NewLine + ((guid != string.Empty) ? GatherQuestions.cStrGatherRequestProcessed + GatherQuestions.cStrGatherRequestProcessedPost + guid : string.Empty) + Environment.NewLine + Environment.NewLine + GatherQuestions.cStrNowSayHi; } // Creates a flight request output. public static string OutputResult(string[] lines, string guid) { string r = string.Empty; foreach (string l in lines) r += l + Environment.NewLine; if (lines.Length > 1) r += SetOutputFooter(guid); return r; } // Main method for processing a flight request. public async Task<string> ProcessRequest() { return await Task.Run(() => { string guid = string.Empty; TripsSearchResponse result = null; string[] res = QpxExpressApiHelper. GetFlightPrices(true, Airports, FlightDetails, out guid, out result).ToArray(); return OutputResult(res, guid); }); } // Destructor ~ProcessFlight() { Dispose(false); } public virtual void Dispose(bool disposing) { if (!disposed) { if (disposing) { FlightDetails = null; } } disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } } |
To understand this code, let’s first examine the JSON data file for a moment. Figure 5.0 shows what a snippet of that data looks like.

Figure 5.0: Snippet of the JSON Data File
Notice that the data contains a key (circled in red) and a value (circled in blue). The key is a string and the value is an object with several properties (represented by the Airport class in the previous code listing).
The Airport class is a C# representation of the data contained within the JSON file hosted on GitHub that contains all the details of the world’s airports.
This will be used to retrieve the IATA codes for the origin and destination of a flight—these are required when querying the QPX Express API.
We use the FlightDetails class as a placeholder to store the data submitted by a user for a specific flight request. It is also used when retrieving the details of a flight that has already been stored and is being followed for price changes. In other words, it is used to manage the state of a flight request, not the bot’s state.
The ProcessFlight class implements the IDisposable interface, and it is responsible for processing a flight request.
This class contains several helper methods that are used to retrieve the list of airports, get IATA codes, and retrieve the city that corresponds to a particular IATA code.
The GetAirports method makes an HTTP request by using a RestClient instance. It deserializes the JSON data retrieved, then returns a Dictionary<string, Airport> object that populates the Airports property.
Two interesting methods of this class are the IsIataCode and GetIataCodes methods. The IsIataCode method loops through the Airports Dictionary and checks whether or not the user’s input corresponds to an IATA code.
On the other hand, the GetIataCodes method is invoked only when the user has not entered an IATA code but instead the name of a city or airport. This method then loops through the Airports Dictionary and checks whether the value entered matches either a city or name of an airport, and then returns the corresponding IATA code(s).
The GetIataCodes method also contains a couple of static methods that are used when the result is displayed to the user. Possible results are OutputResult and SetOutputFooter. Both are string concatenation methods.
These methods are invoked by the ProcessRequest async method, which is used only when the user has submitted all the flight request details.
The ProcessRequest method calls the GetFlightPrices static method from the QpxExpressApiHelper class, which is responsible for invoking the QPX Express API and returning the result of the flight request query.
We’ll explore the QpxExpressApiHelper class later. Notice that we are also using constants defined in a class named GatherQuestions that is defined in the Consts.cs file.
That concludes our mapping of IATA codes. With this finished, we can now focus on the main conversational flow of our bot, from which all requests originate, all validation processes are triggered, and all responses returned for final processing.
One of the most difficult aspects of creating a bot is being able to handle a conversational flow with a user. If we had to handle this logic by ourselves, the complexity of creating a bot would skyrocket.
Luckily, the good folks working at Microsoft’s Bot Framework team have given us a great hand. They have come up with an API that allows us to rather seamlessly create a sequence of events and provide structure for a conversional flow. This API part is known as FormFlow.
FormFlow works by creating a class that contains a few variables and a static method. Each variable represents a question or step in the conversational flow.
The method represents the flow and executes a sequence of subevents (validations), thereby creating the structure and logic for the conversation to follow.
FormFlow handles the validation and conversational state for each step, which means it knows where the conversation is—i.e. at which stage. Validations for each step can be implemented as async methods.
For our bot, the conversational flow logic using FormFlow will be kept in a file called TravelDetails.cs.
Let’s add a new C# class file to our VS project under the Controllers folder, and let’s put in the code from Code Listing 5.2.
Code Listing 5.2: TravelDetails.cs
using AirfareAlertBot.Controllers; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Builder.FormFlow; using System; using System.Threading.Tasks; namespace AirfareAlertBot { // This is the FormFlow class that handles // the conversation with the user, in order to get // the flight details the user is interested in. [Serializable] public class TravelDetails { // Ask the user for the point of origin for the trip. [Prompt(GatherQuestions.cStrGatherQOrigin)] public string OriginIata; // Ask the user for the point of destination for the trip. [Prompt(GatherQuestions.cStrGatherQDestination)] public string DestinationIata; // Ask the user for the outbound trip date. [Prompt(GatherQuestions.cStrGatherQOutboundDate)] public string OutboundDate; // Ask the user for the inbound (return) trip date // (if applicable - if it is not 'one way'). [Prompt(GatherQuestions.cStrGatherQInboundDate)] [Optional] public string InboundDate; // Ask the user for the number of passengers. [Prompt(GatherQuestions.cStrGatherQNumPassengers)] public string NumPassengers; // Ask the user if the flight is direct. [Prompt(GatherQuestions.cStrGatherProcessDirect)] public string Direct; // Ask the user if the flight is to be followed // (check for price changes). [Prompt(GatherQuestions.cStrGatherProcessFollow)] public string Follow; // FormFlow main method, which is responsible for creating // the conversation dialog with the user and validating each // response. public static IForm<TravelDetails> BuildForm() { // Once all the user responses have been gathered // send a response back that the request is being // processed. OnCompletionAsyncDelegate<TravelDetails> processOrder = async (context, state) => { await context.PostAsync( GatherQuestions.cStrGatherProcessingReq); }; // FormFlow object that gathers and handles user responses. var f = new FormBuilder<TravelDetails>() .Message(GatherQuestions.cStrGatherValidData) .Field(nameof(OriginIata), // Validates the point of origin submitted. validate: async (state, value) => { return await Task.Run(() => { Data.currentText = value.ToString(); return FlightFlow.CheckValidateResult( FlightFlow.ValidateAirport(state, Data.currentText, false)); }); }) .Message("{OriginIata} selected") .Field(nameof(DestinationIata), // Validates the point of destination submitted. validate: async (state, value) => { return await Task.Run(() => { Data.currentText = value.ToString(); return FlightFlow.CheckValidateResult( FlightFlow.ValidateAirport(state, Data.currentText, true)); }); }) .Message("{DestinationIata} selected") .Field(nameof(OutboundDate), // Validates the outbound travel date submitted. validate: async (state, value) => { return await Task.Run(() => { Data.currentText = value.ToString(); return FlightFlow.CheckValidateResult( FlightFlow.ValidateDate(state, Data.currentText, false)); }); }) .Message("{OutboundDate} selected") .Field(nameof(InboundDate), // Validates the inbound travel date submitted // (or if it is a one-way trip). validate: async (state, value) => { return await Task.Run(() => { Data.currentText = value.ToString(); return FlightFlow.CheckValidateResult( FlightFlow.ValidateDate(state, Data.currentText, true)); }); }) .Message("{InboundDate} selected") .Field(nameof(NumPassengers), // Validates the number of passengers // submitted for the trip. validate: async (state, value) => { return await Task.Run(() => { Data.currentText = value.ToString(); return FlightFlow.CheckValidateResult( FlightFlow.ValidateNumPassengers(state, Data.currentText)); }); }) .Message("{NumPassengers} selected") .Field(nameof(Direct), // Validates whether the trip is direct or not. validate: async (state, value) => { return await Task.Run(() => { Data.currentText = value.ToString(); return FlightFlow.CheckValidateResult( FlightFlow.ValidateDirect(state, Data.currentText)); }); }) .Message("{Direct} selected") .Field(nameof(Follow), // Validates if the user has submitted // the flight to be // followed for price changes. validate: async (state, value) => { return await Task.Run(() => { Data.currentText = value.ToString(); ValidateResult res = FlightFlow.CheckValidateResult( FlightFlow.ValidateFollow(state, Data.currentText)); FlightFlow. AssignStateToFlightData(res, state); return res; }); }) .Message("{Follow} selected") // When all the data has been // gathered from the user... .OnCompletion(processOrder) .Build(); return f; } } } |
If we take a step back and look at this code, we can see that it’s actually not a lot, and it’s based on method chaining in which each step follows the previous one.
Because FormFlow handles the conversational state, it knows where to pick up the conversation and which validate method to execute. Not only is this awesome, but it’s also convenient and easy to follow. So, let’s dive into the details.
First, notice that the TravelDetails class is decorated with the Serializable attribute.
The TravelDetails class contains a series of variables in which each represents a response that the bot will gather from the user. Each variable is marked with the Prompt attribute that will indicate the question that the user will be asked to answer.
The only optional variable within all the conversation is the one that corresponds to the InboundDate, which is marked with the Optional attribute. This happens in response to requests for flight details without a return trip (i.e. a one-way trip).
The other variables are not optional and are thus required during the conversation. The magic happens inside the BuildForm method. This method controls the conversational flow and the state of the conversation, and it performs validations during each step for each input required. The BuildForm method contains two main parts.
The first part is a delegate called OnCompletionAsyncDelegate<TravelDetails> that gets executed when all the details from the user (represented by the variables within the class) have been gathered.
The second part is a FormBuilder<TravelDetails> instance that gets created and that contains a series of methods that are chained together and executed one after the other following the sequence that we want the conversation to follow.
Most importantly, for each variable defined within the TravelDetails class, FormFlow runs a validate method that can execute any custom code we want invoked, thus validating the value of that the variable—returning a ValidateResult object that determines if FormFlow can continue the conversation or requires the user to re-enter the value for the current variable. This is how conversational state management is achieved.
Conversational state, which includes the values of each of the variables defined within the TravelDetails class, is available to each validate method within each step through the state parameter.
In my opinion, the way FormFlow handles the flow within a conversation and makes it incredibly easy is one of the most important and noticeable features of the Bot Framework.
Be sure to note that FormFlow doesn’t require the validation methods to be asynchronous, but in order to make the bot as responsive as possible, I decided to mark each one as async and write each custom validation logic inside the Run method of a Task object.
The validation logic itself has been abstracted and wrapped up nicely in a separate static class called FlightFlow that we will explore next.
The best part of working with FormFlow is being able to write custom validations for each variable that represents a stage in the conversation.
You could write each validation inside each Validate method of BuildForm, however that would not look particularly good, and it would not be easy to manage as the code base grows. So, my recommendation is to write this logic in a separate class and then invoke it as needed.
In order to do that, let’s create a new C# class file inside our Controllers folder of our VS project and call it FlightFlow.cs.
Code Listing 5.3: FlightFlow.cs
using Microsoft.Bot.Builder.FormFlow; using System; using System.Collections.Generic; namespace AirfareAlertBot.Controllers { // Handles the conversation flow with the users. public partial class FlightFlow { // Interprets an unfollow request command from the user. public static bool ProcessUnfollow(string value, ref ValidateResult result) { bool res = false; if (value.ToLower().Contains( GatherQuestions.cStrUnFollow.ToLower())) { // Use Azure Table storage. using (TableStorage ts = new TableStorage()) { string guid = value.ToLower().Replace( GatherQuestions.cStrUnFollow.ToLower(), string.Empty); // Remove the flight details for the // guide being followed. if (ts.RemoveEntity(guid)) { string msg = GatherQuestions.cStrNoLongerFollowing + guid; result = new ValidateResult { IsValid = false, Value = msg }; result.Feedback = msg; res = true; } else result = new ValidateResult { IsValid = false, Value = GatherErrors.cStrNothingToUnfollow }; } } return res; } // Validates that the response to the user is never empty. public static ValidateResult CheckValidateResult( ValidateResult result) { result.Feedback = ((result.Feedback == null || result.Feedback == string.Empty) && result.Value.ToString() == string.Empty) ? GatherQuestions.cStrGatherValidData : result.Feedback; return result; } // Validates the user's response for a direct flight (or not). public static ValidateResult ValidateDirect(TravelDetails state, string value) { ValidateResult result = new ValidateResult { IsValid = false, Value = string.Empty }; string direct = (value != null) ? value.Trim() : string.Empty; if (ProcessUnfollow(direct, ref result)) return result; if (direct != string.Empty) { // If direct flight response is correct. if (direct.ToLower() == GatherQuestions.cStrYes || direct.ToLower() == GatherQuestions.cStrNo) return new ValidateResult { IsValid = true, Value = direct.ToUpper() }; else { if (result.Feedback != null) result.Feedback = GatherQuestions.cStrGatherProcessTryAgain; } } else { if (result.Feedback != null) result.Feedback = GatherQuestions.cStrGatherProcessTryAgain; } return result; } // Validates the user's response to follow a flight request // (for price changes). public static ValidateResult ValidateFollow( TravelDetails state, string value) { ValidateResult result = new ValidateResult { IsValid = false, Value = string.Empty }; string follow = (value != null) ? value.Trim() : string.Empty; if (ProcessUnfollow(follow, ref result)) return result; if (follow != string.Empty) { // If the response to follow a flight is correct. if (follow.ToLower() == GatherQuestions.cStrYes || follow.ToLower() == GatherQuestions.cStrNo) return new ValidateResult { IsValid = true, Value = follow.ToUpper() }; else { if (result.Feedback != null) result.Feedback = GatherQuestions.cStrGatherProcessTryAgain; } } else result.Feedback = GatherQuestions.cStrGatherRequestProcessedNoGuid; return result; } // Validates the user's number of passengers response. public static ValidateResult ValidateNumPassengers( TravelDetails state, string value) { ValidateResult result = new ValidateResult { IsValid = false, Value = string.Empty }; string numPassengers = (value != null) ? value.Trim() : string.Empty; if (ProcessUnfollow(numPassengers, ref result)) return result; if (numPassengers != string.Empty) { // Verifies the number of passengers and if correct. result = ValidateNumPassengerHelper. ValidateNumPassengers(numPassengers); if (!result.IsValid && (result.Feedback == null || result.Feedback == string.Empty)) result.Feedback = GatherQuestions.cStrGatherProcessTryAgain; } else { if (result.Feedback != null) result.Feedback = GatherQuestions.cStrGatherProcessTryAgain; } return result; } // Validates the user's travel date // (outbound or inbound) response. public static ValidateResult ValidateDate(TravelDetails state, string value, bool checkOutInDatesSame) { ValidateResult result = new ValidateResult { IsValid = false, Value = string.Empty }; string date = (value != null) ? value.Trim() : string.Empty; if (ProcessUnfollow(date, ref result)) return result; if (checkOutInDatesSame && value.ToLower().Contains( GatherQuestions.cStrGatherProcessOneWay)) return new ValidateResult { IsValid = true, Value = GatherQuestions.cStrGatherProcessOneWay }; if (date != string.Empty) { DateTime res; // If it is a proper date. if (DateTime.TryParse(value, out res)) { if (checkOutInDatesSame) { // Performs the actual date validation. result = ValidateDateHelper. ValidateGoAndReturnDates(ValidateDateHelper. ToDateTime(state.OutboundDate), ValidateDateHelper.ToDateTime(value), ValidateDateHelper. FormatDate(value)); } else // If it is a date in the future. result = ValidateDateHelper.IsFutureDate( ValidateDateHelper.ToDateTime(value), ValidateDateHelper.FormatDate(value)); } else { if (result.Feedback != null) result.Feedback = GatherQuestions.cStrGatherProcessTryAgain; } } // If it is not a proper date. else { if (result.Feedback != null) result.Feedback = GatherQuestions.cStrGatherProcessTryAgain; } return result; } // Validates the user's response for origin and destination. public static ValidateResult ValidateAirport(TravelDetails state, string value, bool checkOrigDestSame) { bool isValid = false; List<string> values = new List<string>(); ValidateResult result = new ValidateResult { IsValid = false, Value = string.Empty }; string city = (value != null) ? value.Trim() : string.Empty; if (ProcessUnfollow(city, ref result)) return result; if (city != string.Empty) { // Get the IATA code (if any) corresponding // to the user input. isValid = Data.fd.IsIataCode(value.ToString(), ref values); if (isValid) { // Processes the IATA code response. result = ValidateAirportHelpers. ProcessAirportIataResponse(state, checkOrigDestSame, values.ToArray()); } // When multiple airports are found for a given city. else { // Get all the IATA codes for all the // airports in a city. string[] codes = InternalGetIataCodes(value.ToString().Trim());
if (codes.Length == 1) { // When the specific match is found. result = ValidateAirportHelpers. ProcessAirportResponse(state, checkOrigDestSame, codes); } else if (codes.Length > 1) { // When multiple options are found. result = new ValidateResult { IsValid = isValid, Value = string.Empty }; result = ValidateAirportHelpers. GetOriginOptions(codes, result); } else { if (result.Feedback != null) { result = new ValidateResult { IsValid = isValid, Value = string.Empty }; result.Feedback = GatherQuestions. cStrGatherProcessTryAgain; } } } } return result; } } } |
Let’s analyze this code to understand what is happening. First, notice that this is a partial class, which means there’s another part of the class contained within another .cs file that we will examine shortly.
This separation of classes makes the code easier to read and follow, and it makes the code more manageable. However, it is not a must.
This partial class is not making any use of the Bot Framework’s features, so it is merely acting as a container for the validation logic that the BuildForm method invokes when validating each step in the flow of conversation. It’s a way to keep our main validation logic all in one place.
The first method within this class is called ProcessUnfollow, and it removes a flight request that has been saved on Azure Storage (in order to receive price alert changes), so it is no longer watched.
This method is executed on all validations, as it is possible for a user to issue an unfollow command during each step of the conversation.
Following that, the next method is called CheckValidateResult. Its purpose is to ensure that any ValidateResult object returned by any of the validation steps does not return an empty Feedback property. When the IsValid property of ValidateResult is false, the bot is prevented from sending back an empty string response.
The ValidateFollow method checks if the user has typed a valid response for the follow question (if a flight request is to be followed)—either a Yes or a No.
The ValidateDirect method does the same for the direct question (whether a flight is direct or it allows multiple connections).
The ValidateNumPassengers method validates if the number of passengers is a number between 1 and 100.
The ValidateDate method is slightly more complex, as it not only verifies that the OutboundDate and InboundDate (if applicable) are correct, but it also checks that the dates are not in the past and that the InboundDate is a future date compared to the OutboundDate.
Finally, the most interesting and complex validation method within this partial class is the ValidateAirport method. This is responsible for validating that the origin and destination airports are both correct—that they exist (are valid IATA codes) and, if the user has indicated multiple airports for the city, that the existing options are returned so that the user can choose any option.
Notice how all these validation methods return a ValidateResult object. Even though their logic seems quite straightforward, there’s still some code required in order to carry out a proper validation.
In this sense, and to keep the code as clean as possible, you’ve probably noticed that some validation methods internally invoke their own validation helper methods (which are contained in separate classes). These are responsible for handling very specific validation logic for each of the steps in the conversation flow. We’ll see this code later.
Some of these validation helper classes are ValidateNumPassengerHelper, ValidateDateHelper, and ValidateAirportHelpers.
With FlightFlow.cs out of the way, let’s explore the remaining bits of the FlightFlow partial class. I’ve placed this in a C# class file called FlightFlowData.cs.
Code Listing 5.4: FlightFlowData.cs
using Microsoft.Bot.Builder.FormFlow; using Microsoft.Bot.Connector; namespace AirfareAlertBot.Controllers { // Keeps the state of the conversation. public class Data { public static ProcessFlight fd = null; public static StateClient stateClient = null; public static string channelId = string.Empty; public static string userId = string.Empty; public static Activity initialActivity = null; public static ConnectorClient initialConnector = null; public static string currentText = string.Empty; } public partial class FlightFlow { // Gets relevant IATA codes for a user's response. private static string[] InternalGetIataCodes(object value) { string find = value.ToString().Trim(); string[] codes = null; codes = Data.fd.GetIataCodes(string.Empty, find, string.Empty); if (codes.Length == 0) codes = Data.fd.GetIataCodes(find, string.Empty, string.Empty); return codes; } // Set the bot's state as the internal state. public static void AssignStateToFlightData( ValidateResult result, TravelDetails state) { if (result.IsValid) { string userId = Data.fd.FlightDetails.UserId; Data.fd.FlightDetails = new FlightDetails() { OriginIata = state.OriginIata, DestinationIata = state.DestinationIata, OutboundDate = state.OutboundDate, InboundDate = state.InboundDate, NumPassengers = state.NumPassengers, NumResults = "1", Direct = state.Direct, UserId = userId, Follow = result.Value.ToString() }; } } } } |
The remaining bit of the FlightFlow class contains just a few methods.
The InternalGetIataCodes method is responsible for retrieving the corresponding IATA codes based on the user’s response. It is invoked by the ValidateAirport method.
AssignStateToFlightData simply assigns the value of the bot’s state to an instance of FlightDetails that is assigned to the Data.fd.FlightDetails property, which will be used mostly by the CheckForPriceUpdates method.
This wraps up the main custom validation logic. Imagine for a moment that all this code had been placed inside the validation methods of BuildForm within the TravelDetails class. That would have been quite a mess. The code would be incredibly difficult to understand and maintain. However, with this approach, we have a clear separation of concerns and code readability.
Before totally closing off validations, let’s have a look at each of the validation helper classes described so far, starting with the code for ValidateAirportHelpers.cs.
Code Listing 5.5: ValidateAirportHelpers.cs
using Microsoft.Bot.Builder.FormFlow; using System; namespace AirfareAlertBot.Controllers { // A set of helper methods used to validate // origin and destination airports. public class ValidateAirportHelpers { // Checks that the origin and destination are not the same. private static ValidateResult CheckOrigDestState( TravelDetails state, string field) { ValidateResult result = new ValidateResult { IsValid = false, Value = string.Empty }; result.Feedback = GatherErrors.cStrGatherSameCities; if (state?.OriginIata.ToLower() != field.ToLower()) result = new ValidateResult { IsValid = true, Value = field }; return result; } // Checks that the IATA code submitted by the // user for origin or destination is a valid code. public static ValidateResult ProcessAirportIataResponse( TravelDetails state, bool checkOrigDestSame, string[] items) { string field = string.Empty; string[] airport = new string[] { items[0] + "|" + Data.fd.GetAirportCity(items[0]) }; ValidateResult result = ProcessPrefix(state, checkOrigDestSame, airport, out field); result.Feedback = (result.IsValid) ? field : GatherErrors.cStrGatherSameCities; return result; } // Part of the validation of the origin and destination checks. private static ValidateResult ProcessPrefix(TravelDetails state, bool checkOrigDestSame, string[] codes, out string field) { string[] code = codes[0].Split('|'); string prefix = !checkOrigDestSame ? "Origin" : "Destination"; field = $"{prefix}: {code[1]} ({code[0]})"; ValidateResult result = (checkOrigDestSame) ? CheckOrigDestState(state, code[0]) : new ValidateResult { IsValid = true, Value = code[0] }; return result; } // Part of the validation of the origin and destination checks. public static ValidateResult ProcessAirportResponse( TravelDetails state, bool checkOrigDestSame, string[] codes) { string field = string.Empty; ValidateResult result = ProcessPrefix(state, checkOrigDestSame, codes, out field); result.Feedback = (result.IsValid) ? field : GatherErrors.cStrGatherSameCities; return result; } // Show the user the various airport options available. public static ValidateResult GetOriginOptions(string[] values, ValidateResult result) { result.Feedback = GatherErrors.cStrGatherMOrigin + Environment.NewLine + Environment.NewLine; foreach (string o in values) { string[] parts = o.Split('|'); string newLine = $"{parts[0]} = {parts[1]}"; result.Feedback += newLine + Environment.NewLine + Environment.NewLine; } return result; } } } |
We use these methods to validate the data associated with the origin and destination airports. Let’s go over each one of them briefly.
The CheckOrigDestState method checks that the origin and destination airports entered by the user are not the same.
The ProcessAirportIataResponse method checks that the IATA codes for origin and destination are actually valid. This is done in conjunction with the ProcessPrefix method, which is private.
The ProcessAirportResponse method is similar to ProcessAirportIataResponse, with the difference being that ProcessAirportResponse is invoked after multiple airports have been found in a specific city.
Finally, the GetOriginOptions method is used to show the user which airport options have been found, which allows the user to choose one.
That concludes the ValidateAirportHelpers class. Let’s now explore ValidateDateHelper.cs.
Code Listing 5.6: ValidateDateHelper.cs
using Microsoft.Bot.Builder.FormFlow; using System; using System.Collections.Generic; namespace AirfareAlertBot.Controllers { // This helper class is used to validate trip dates. public class ValidateDateHelper { // Parses and converts a string date to a DateTime date. public static DateTime ToDateTime(string date) { return DateTime.Parse(date); } // Determines the number of days between two dates. private static int DaysBetween(DateTime d1, DateTime d2) { TimeSpan span = d2.Subtract(d1); return (int)span.TotalDays; } // Formats the date to a more human readable way ;) public static string FormatDate(string dt) { string res = dt; List<string> nParts = new List<string>(); string tmp = dt.Replace("/", "-"). Replace(" ", "-").Replace(".", "-"); string[] parts = tmp.Split('-'); if (parts?.Length > 0) { int j = 1; foreach (string str in parts) { if (str != "-") { string t = string.Empty; if (j == 2) t = str.Substring(0, 3); else t = str; nParts.Add(t); } j++; } if (nParts.Count > 0) { int i = 1; List<string> pParts = new List<string>(); foreach (string p in nParts) { if (p.Length < 2 && (i == 1)) pParts.Add(p.PadLeft(2, '0')); else if (p.Length == 2 && (i == 3)) { DateTime n = ToDateTime(dt); pParts.Add(n.Year.ToString()); } else pParts.Add(p); i++; } res = string.Join("-", pParts.ToArray()).ToUpper(); } } return res; } // Checks if a date is in the future. public static ValidateResult IsFutureDate(DateTime dt, string value) { ValidateResult result = new ValidateResult { IsValid = false, Value = string.Empty }; if (DaysBetween(DateTime.Now, dt) >= 0) result = new ValidateResult { IsValid = true, Value = value }; else result.Feedback = GatherErrors.cStrGatherStatePastDate; return result; } // Main method responsible for validating trip dates. public static ValidateResult ValidateGoAndReturnDates(DateTime go, DateTime comeback, string value) { ValidateResult result = new ValidateResult { IsValid = false, Value = string.Empty };
result = new ValidateResult { IsValid = false, Value = string.Empty }; if (DaysBetween(go, comeback) >= 0) result = new ValidateResult { IsValid = true, Value = value }; else result.Feedback = GatherErrors.cStrGatherStateFutureDate; return result; } } } |
The main purposes of this helper class are to ensure that date validations are correct and to format the date as an easily readable response. Let’s briefly go over the methods of this class.
The first two methods, ToDateTime and DaysBetween, convert a string date into DateTime object and calculate the number of days between two dates, respectively.
The FormatDate method, as its name implies, formats the date string, which makes it easier for humans to read.
The method IsFutureDate checks whether a date is in the future. This is quite an important requirement when looking for a flight.
Finally, ValidateGoAndReturnDates is the main method for validating the outbound and inbound trip dates. The main check it performs is verifying that the difference between the inbound and outbound dates is at least on the same day or in the future.
That concludes the ValidateDateHelper class. Let’s now check ValidateNumPassengerHelper.cs.
Code Listing 5.7: ValidateNumPassengerHelper.cs
using Microsoft.Bot.Builder.FormFlow; using System; namespace AirfareAlertBot.Controllers { public class ValidateNumPassengerHelper { // Validates the number of passenger responses. public static ValidateResult ValidateNumPassengers(string value) { ValidateResult result = new ValidateResult { IsValid = false, Value = string.Empty }; try { int res = Convert.ToInt32(value); if (res >= 1 && res <= 100) result = new ValidateResult { IsValid = true, Value = value }; else result.Feedback = GatherErrors.cStrGatherStateInvalidNumPassengers; } catch { } return result; } } } |
This class contains one method called ValidateNumPassengers that checks that the number of passengers is a valid integer number between 1 and 100.
With these validations explained, let’s now move our attention to MessagesController.cs, which is where the bot receives the messages from users and replies to them.
The MessagesController.cs file is the main entry point for our bot to receive and send messages.
When we selected the Bot Application template to create our VS solution, some very basic code was added by default to the MessagesController.cs file (which we explored in Chapter 2).
However, given that our bot is now quite complex, let’s remove that boilerplate code and use this one.
Code Listing 5.8: MessagesController.cs
using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; using System.Web.Http; using Microsoft.Bot.Connector; using AirfareAlertBot.Controllers; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Builder.FormFlow; using System.Timers; using Microsoft.Azure; using System.Web.Http.Controllers; namespace AirfareAlertBot { // Main class responsible for main user-to-bot interactions. [BotAuthentication] public class MessagesController : ApiController { protected bool disposed; // Timer used to check for flight price changes. private static Timer followUpTimer = null; // Set to true when flight price changes are checked. private static bool timerBusy = false; // Set to true when user-to-bot interaction is ongoing. private static bool msgBusy = false; // Initializes the flight price changes check Timer. private static void SetFollowUpTimer() { if (followUpTimer == null) { followUpTimer = new Timer(); followUpTimer.Elapsed += new ElapsedEventHandler(OnTimedEvent); double interval = 10000; try { string fupInterval = CloudConfigurationManager.GetSetting( StrConsts.cStrFollowUpInterval); interval = Convert.ToDouble(fupInterval); } catch { } followUpTimer.Interval = interval; followUpTimer.Enabled = true; timerBusy = false; } } // Main method for running the flight price changes check. private static Task ProcessTimer() { return Task.Run(async () => { return await Task.Run(async () => { bool changed = false; // The bot checks in Azure Table storage... using (TableStorage ts = new TableStorage()) { // ...If a stored flight request has had any // price changes // and if so, send the user this information... changed = await ts.CheckForPriceUpdates( Data.fd.Airports, Data.fd.FlightDetails, Data.initialActivity, Data.initialConnector, Data.currentText); timerBusy = false; } return changed; }); }); } // Triggers the flight price changes check. private static void OnTimedEvent(object source, ElapsedEventArgs e) { if (!timerBusy && !msgBusy) { timerBusy = true; ProcessTimer(); } } // FormFlow end method: Executed once the request // details have been gathered from the user. internal static IDialog<TravelDetails> MakeRootDialog() { return Chain.From(() => FormDialog.FromForm( TravelDetails.BuildForm, FormOptions.None)) .Do(async (context, order) => { try { var completed = await order; // Request processed. string res = await Data.fd.ProcessRequest(); // Request result sent to the user. await context.PostAsync(res); Data.fd.FlightDetails = null; } // This also gets executed when a 'quit' command // is issued. catch (FormCanceledException<TravelDetails> e) { Data.fd.FlightDetails = null; string reply = string.Empty; if (e.InnerException == null) reply = GatherQuestions.cStrQuitMsg; else reply = GatherErrors.cStrShortCircuit; await context.PostAsync(reply); } }); } // Inits the bot's conversational internal state. private void InitState(Activity activity) { if (Data.stateClient == null) Data.stateClient = activity.GetStateClient(); if (Data.fd.FlightDetails == null) Data.fd.FlightDetails = new FlightDetails(); Data.fd.FlightDetails.UserId = activity.From.Id; } // Inits and invokes the ProcessFlight constructor. private void InitFlightData() { if (Data.fd == null) Data.fd = new ProcessFlight(); } // Gets the user and channel IDs of the conversation. private void GetUserAndChannelId(Activity activity, ConnectorClient connector) { Data.channelId = activity.ChannelId; Data.userId = activity.From.Id; Data.initialActivity = activity; Data.initialConnector = connector; } ~MessagesController() { Data.fd.Dispose(); } // Send the bot's default welcome message to the user. private async void SendWelcomeMsg(ConnectorClient connector, Activity activity) { if (Data.fd == null) { Activity reply = activity.CreateReply( GatherQuestions.cStrWelcomeMsg); await connector.Conversations. ReplyToActivityAsync(reply); } } // Initializes the bot. protected override void Initialize(HttpControllerContext controllerContext) { base.Initialize(controllerContext); SetFollowUpTimer(); } // Process an unfollow command outside a conversation. private async void ProcessUnfollow(ConnectorClient connector, Activity activity) { if (activity.Text.ToLower().Contains( GatherQuestions.cStrUnFollow)) { ValidateResult r = new ValidateResult { IsValid = false, Value = GatherErrors.cStrNothingToUnfollow }; if (FlightFlow.ProcessUnfollow(activity.Text, ref r)) { Activity reply = activity.CreateReply(r.Feedback); await connector.Conversations. ReplyToActivityAsync(reply); } } }
// This is the bot's main entry point for all user responses. public async Task<HttpResponseMessage> Post([FromBody]Activity activity) { try { // Inits the flight price change check timer. SetFollowUpTimer(); // Set the user-to-bot conversational status as ongoing. msgBusy = true; // Inits the Bot Framework Connector service. ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl)); // Gets the user and channel IDs. GetUserAndChannelId(activity, connector); // When the user has typed a message if (activity.Type == ActivityTypes.Message) { // Let's greet the user. SendWelcomeMsg(connector, activity); // Init the state and flight request. InitFlightData(); InitState(activity); ProcessUnfollow(connector, activity); // Send the FormBuilder conversational dialog. await Conversation.SendAsync(activity, MakeRootDialog); } else await HandleSystemMessage(connector, activity); } catch { } var response = Request.CreateResponse(HttpStatusCode.OK); // A response has been sent back to the user. msgBusy = false; return response; } private Task<Activity> HandleSystemMessage(ConnectorClient connector, Activity message) { // Not used for now... // Here put any logic that gets on any of these. // Activity Types if (message.Type == ActivityTypes.DeleteUserData) { } else if (message.Type == ActivityTypes.ConversationUpdate) { } else if (message.Type == ActivityTypes.ContactRelationUpdate) { } else if (message.Type == ActivityTypes.Typing) { } else if (message.Type == ActivityTypes.Ping) { } return null; } } } |
Now, let’s understand what is going on here.
First and foremost, the MessagesController class implements the ApiController interface. As we’ve seen, this application is nothing more than a slightly modified WebApi project.
Three private static variables have been declared—followUpTimer, timerBusy, and msgBusy.
The variable followUpTimer is of type System.Timers.Timer, which is a timer that gets executed every FollowUpInterval (found in the Web.config file) number of milliseconds.
The sole purpose of the followUpTimer object is to execute code that checks if price changes have occurred for flights that are being watched and inform the user that is following them.
The timerBusy variable is used as a semaphore so that the code being executed when followUpTimer goes off doesn’t get triggered a second time.
The msgBusy variable also acts as a semaphore—it indicates that communication between the user and the bot is taking place. It is also used to prevent execution of any other code during that moment.
The method SetFollowUpTimer is used to initialize the followUpTimer. This method sets up all the necessary parameters that followUpTimer requires.
The ProcessTimer method is responsible for running all the code that checks if flights being followed by the user have had any price changes and informing the user in such an event.
The OnTimedEvent method, as its name implies, is triggered every FollowUpInterval number of milliseconds. It invokes the ProcessTimer method.
At this stage, we are mostly done with the code that checks for flight price changes. Here comes the interesting bit—next on the list is the MakeRootDialog method. This is a unique and interesting method because it is required when we work with FormFlow. It creates a FormDialog instance based on the IForm<TravelDetails> object returned by TravelDetails.BuildForm. In other words, it literally creates the FormFlow dialog.
The built-in Do method contained within MakeRootDialog gets executed when all the flight request details have been gathered from the user. There are two parts to this Do method.
The first part, contained within the try section, gets executed when all the details from the flight request have been gathered from the user. This leads to the execution of the actual request itself, which is accomplished by invoking Data.fd.ProcessRequest. The result of ProcessRequest is returned to the user when context.PostAsync gets called.
The second part, contained within the catch section, gets executed either when an exception within the try section occurs or when the user cancels the request by typing the word quit. If e.InnerException is null, the user has cancelled the request. In any case, a response is sent back to the user by calling context.PostAsync.
The InitState method initializes the bot’s internal conversational state. This is necessary in order for the bot to start off with a clean slate with the user.
The InitFlightData method is responsible for creating an instance of the ProcessFlight class that will be used throughout the bot’s lifecycle to process flight requests.
The GetUserAndChannelId method sets the channelId, userId, initialActivity, and initialConnector properties, which are needed to check for flight price changes.
The SendWelcomeMsg method is super simple—it creates an Activity instance called reply that is sent as a response to the user when the conversation starts.
Keep in mind that the user always starts the conversation by sending a message to the bot.
The Initialize method invokes the inherited Initialize method from ApiController and calls the SetFollowUpTimer method.
The ProcessUnfollow method executes an unfollow command outside of a conversation when the previous conversation has finalized (and before a new conversation has started). This method is invoked within the Post method.
The Post method is another crucial function for the bot. This is the main entry point for all user input.
Within the Post method, several initialization methods are called. We’ve previously examined these, so we know when a user has sent a message because the type of activity.Type is equal to ActivityTypes.Message. There are several other ActivityTypes that can be handled separately within the HandleSystemMessage method, but these are not used by our bot.
The really interesting part of the Post method comes when it internally invokes the Conversation.SendAsync method. I mean interesting in the sense that an activity instance is passed as a parameter (which corresponds to the user’s input) along with a callback to the MakeRootDialog method.
MakeRootDialog creates the FormFlow instance that handles all the conversational flow with the user and keeps track of the state of the conversation. In essence, every time the Conversation.SendAsync method is called, the Post method passes the user’s input (represented by the activity instance) to the FormFlow instance that is handling the entire conversation (along with its corresponding validations).
This separation of concerns is, in my opinion, the epitome of design excellence from the Bot Framework team. It makes the code readable and easy to manage, and it allows the code base to grow while being nicely organized, which makes the creation of any bot possible.
Those were the bot’s main code parts! A lot of things are going on there. Creating a bot is not a trivial task, and there are a few complexities associated with the process, such as handling its very asynchronous nature and also managing state. The Bot Framework makes it easier to handle both aspects by allowing your code to scale.
With our bot ready, it’s now time to give it a whirl and see its nice results. I tested the bot quite extensively while writing this e-book, in part because I also wanted to make a tool that I could use myself. Here, I run a couple of requests to show you what output we get.
I’ll publish it as a Skype bot (privately, not listed on the Bot Directory) in order to run a couple of example flight requests. However, I encourage you to try it out with the Bot Channel Emulator first.
We’ve covered the steps required to register a bot on the developer portal and publish it on Azure App Services, so we’ll skip that part here.
I’ve registered the bot on the developer portal, added the MicrosoftAppId and MicrosoftAppPassword, published the bot to Azure App Services, and added it as a Skype contact.
Let’s see some example interaction.

Figure 5.1: Running Our Bot—Gathering Data (Screenshot 1)
The first thing I do is greet the bot by saying “hi” (actually, any word does the trick at this point).
The bot replies with a greeting and asks for some valid data, such as the point of origin. I reply with the name of my departure city—in this case, Amsterdam (IATA code: AMS).
When the bot has the point of origin confirmed, it requests the point of destination. In this case, I type the IATA code for the airport of my destination—Alicante (IATA code: ALC).

Figure 5.2: Running Our Bot—Gathering Data (Screenshot 2)
With the origin and destination confirmed, the bot will ask for the travel date and a return date (if applicable). In my case, I specify 31-DEC-2016 (as the outbound date) and 08-JAN-2017 (as the inbound date), respectively.
Notice that I entered both travel dates with a slightly different format and the bot formatted them to DD-MMM-YYYY.
When the dates have been confirmed, the bot asks for the number of passengers. In my case, I enter 1.
Following that, the bot asks if the flights are direct. I specify my response as “no.”

Figure 5.3: Running Our Bot—Gathering Data (Screenshot 3)
Next, the bot asks if I would like to follow this trip for price changes. I reply “yes” to that. With all the details gathered, the bot lets me know the request is being processed.
When the flight request has been processed, I receive the feedback in Figure 5.4.

Figure 5.4: Running Our Bot—Results (Screenshot 1)
Because I requested that the flights be followed for price changes, the bot returned a RequestId that I can later use to unfollow it (when I am no longer interested in it).
All of the flight details were provided immediately below the RequestId. We can see further details of the inbound flight in Figure 5.5.

Figure 5.5: Running Our Bot—Results (Screenshot 2)
Awesome! The bot has given me the travel details I wanted. Notice that at the end of a request, if we have chosen to follow a flight, the bot will also add a few extra lines explaining how to later unfollow the request.

Figure 5.6: Running Our Bot—Results (Screenshot 3)
The request has been processed and the bot expects a greeting (such as “hi” or any other word) to start with a new request (a new conversation). At this point, you can also unfollow a trip (or you can do this later when a new conversation starts).
I decide to unfollow the trip as I am no longer interested in it, and I type the unfollow command followed by the RequestId I received.
The command should follow this convention, with a space between the word “unfollow” and the RequestId:
unfollow RequestId
The request ID is one long string, i.e.:
ams_alc_31-dec-2016_08-jan-2017_yes_1_29:1pjyh9sskjerg9al5mpw8jhqnjmyxyni5xjsxw33sk-o

Figure 5.7: Unfollowing a Flight Request
The bot then replies by telling us that the flight has been unfollowed. The original follow request is then removed from Azure Storage.
Because the RequestId is so long, it's not very user-friendly. An enhancement you might add is creating a short RequestId alias such as "ABCDEF."
In order to show you what a price change notification looks like, I manually edited the entry on Azure Storage where this information is stored, and I changed the price. Figure 5.8 shows what the bot returned.

Figure 5.8: A Price Change Update Message
The QPX Express API’s results are ordered by prices, so you always get the best possible price for any given route. That’s very cool.
However, be sure to keep in mind that sites such Google Flights use the full-blown QPX API (rather than QPX Express), which means you might get a slightly better price from Google Flights. However, during my tests, I was blown away with the results from the QPX Express API—they almost always matched the Google Flights results. So, it is a very accurate API.
Regarding the AirfareAlertBot source code, you might use it for tracking your own flights, and you are welcome to make any improvements. If you do so, I’d love to hear from you. However, you cannot use this code commercially, and you are not allowed to modify this code and create a modified bot application for profit. Usage of this code is solely permitted for personal and educational purposes.
We’ve covered a lot of ground in this e-book, and it’s been one of the most interesting subjects I’ve ever had the fortune to explore.
Bots are just getting started, and this is a whole new area of software development that has been getting a lot of media attention lately. Some companies have started to openly embrace it, either by creating their own bots or by providing developers a framework for creating them.
Of the various bot libraries and frameworks I’ve had the chance to put my hands on, the Microsoft Bot Framework is my favorite. The set of APIs provided out-of-the-box, and the clean code it allows you to write, are both absolutely wonderful advantages that this framework offers.
But that’s not all. There are other components within the Microsoft stack that have largely come out of Microsoft Research, such as Cognitive Services, QnA Maker, and LUIS, that can take bot creation to the next level, adding further capabilities such as natural language processing.
If bots get you excited, I definitely recommend that you explore these other services, and why not expand the concepts presented here and improve on them?
For me, learning more about bots has been a lovely experience, even though it was incredibly challenging for me at some points.
I hope you have enjoyed reading this e-book as much as I have loved writing it. With a desire that this information will inspire any further interests in bots you might have, thanks so much for following along. Maybe we can follow up in the future with more bot fun.
Cheers, and happy bot hacking! All the best, Ed.