Trivia games provide an engaging and educational experience where you can learn new facts and expand your knowledge across various subjects. Nowadays, Trivia quiz mobile and web applications are the most common go-to areas for such an activity. How about playing a trivia game on WhatsApp? In this tutorial guide, you will learn how to create a trivia quiz application using Twilio for WhatsApp, ASP.NET Core, and ( ). The aim of this tutorial is to create a trivia game application that allows users to play and answer multiple-choice questions using WhatsApp. You will leverage to store and retrieve user progress, track score and maintain game state. The Trivia API licensed under CC BY-NC 4.0 sessions in ASP.NET Core To fetch these questions, you will be using The Trivia API, a REST API, that makes it easy for developers to build quiz apps by providing multiple-choice trivia questions. To learn more about the Trivia API, please visit the__ __. Trivia API documentation Prerequisites To complete this tutorial, you will need: An OS that supports .NET (Windows/macOS/Linux) .NET 7 SDK A code editor or IDE (Recommended: , with the , or ) Visual Studio Visual Studio Code C# plugin JetBrains Rider ngrok CLI A free or paid Twilio Account (If you don't have one, you can ) try Twilio for free Experience with C# and ASP.NET Core . The source code for this tutorial can be found on GitHub Set up a new ASP.NET Core project To get started, using your shell terminal in a preferred working directory, run the following commands to create a new web API project: dotnet new webapi -n TwilioWhatsAppTriviaApp --no-openapi The second command in the snippet above will create a new web API project with the specified name and without OpenAPI (Swagger) support. If you want to use Swagger in the project, just omit in the command above. --no-openapi Change into the project directory by running this command: cd TwilioWhatsAppTriviaApp Install the NuGet package: Twilio Helper library for ASP.NET Core dotnet add package Twilio.AspNet.Core This library simplifies working with Twilio webhooks and APIs in an ASP.NET Core application. Open the project using your preferred IDE. In the folder, remove the boilerplate template controller file, , and also remove the in the project directory. Controllers WeatherForecastController.cs WeatherForcast.cs Build and run your project to ensure everything you have done so far works well using the following commands: dotnet build dotnet run After successfully running the project, take note of any of the localhost URLs that appear in the debugging console. You can use any of these URLs to set up a publicly accessible local web server using ngrok. Implement Sessions Sessions are one of the several ways of storing a user’s data in an ASP.NET Core application. This is essential when you want to retain user data between requests because by default, the HTTP protocol is stateless - that means data is not preserved. Add the in-memory session provider by modifying , as shown in the following code: Program.cs var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddDistributedMemoryCache(); builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromSeconds(40); options.Cookie.IsEssential = true; }); var app = builder.Build(); app.UseSession(); app.MapControllers(); app.Run(); registers the distributed memory cache service. This service provides an in-memory cache that can be used to store and retrieve data across multiple requests or sessions. AddDistributedMemoryCache() registers the session services, enabling the application to maintain session state. The parameter allows you to configure various session-related options. is used to set the duration of inactivity after which a session will be considered idle. In this case, it is set to 40 seconds. ensures that the session’s state remains functional and even in scenarios where cookie rejection is enabled. AddSession() options IdleTimeout Cookie.IsEssential Session support is enabled by adding the middleware to the application pipeline, that is, your application gains access to a session object that can be used to store and retrieve data. UseSession Create the Models Create a new folder, in your project directory. Add two model class files, and with properties as shown in the following code samples: Models TriviaApiResponse.cs Question.cs using Newtonsoft.Json; namespace TwilioWhatsAppTriviaApp.Models; public class TriviaApiResponse { [JsonProperty("category")] public string Category { get; set; } [JsonProperty("correctAnswer")] public string CorrectAnswer { get; set; } [JsonProperty("incorrectAnswers")] public List<string> IncorrectAnswers { get; set; } [JsonProperty("question")] public string Question { get; set; } [JsonProperty("type")] public string? Type { get; set; } [JsonProperty("difficulty")] public string Difficulty { get; set; } } namespace TwilioWhatsAppTriviaApp.Models; public class Question { public string QuestionText { get; set; } public List<(string option, bool isCorrect)> Options { get; set; } } The model includes properties that represent the fields of The Trivia API response. The attribute ensures that each property is correctly populated with the corresponding JSON data. TriviaApiResponse JsonProperty For a streamlined way to handle trivia questions, the class comes to the rescue. This class encapsulates the necessary information for a trivia question, including the question text and a list of options. Each option is represented by a tuple containing the option text and a boolean value indicating whether it is the correct option. Question Add the Trivia Service Class Create a folder in your project directory and add a new class file named . Modify its contents, as shown in the following code: Services TriviaService.cs using Newtonsoft.Json; using TwilioWhatsAppTriviaApp.Models; namespace TwilioWhatsAppTriviaApp.Services; public class TriviaService { private const string TheTriviaApiUrl = @"https://the-trivia-api.com/api/questions?limit=3"; private HttpClient httpClient; public TriviaService(HttpClient httpClient) { this.httpClient = httpClient; } public async Task<IEnumerable<TriviaApiResponse>> GetTrivia() { var response = await httpClient.GetAsync(TheTriviaApiUrl); var triviaJson = await response.Content.ReadAsStringAsync(); var trivia = JsonConvert.DeserializeObject<IEnumerable<TriviaApiResponse>>(triviaJson); return trivia; } public List<Question> ConvertTriviaToQuestions(IEnumerable<TriviaApiResponse> questions) { List<Question> newQuestions = new(); foreach (var question in questions) { var options = new List<(string option, bool isCorrect)>() { (question.CorrectAnswer, true), (question.IncorrectAnswers[0], false), (question.IncorrectAnswers[1], false), (question.IncorrectAnswers[2], false) }; // Shuffle the options randomly Random random = new(); options = options.OrderBy(_ => random.Next()).ToList(); newQuestions.Add(new Question { QuestionText = question.Question, Options = options }); } return newQuestions; } } The class contains two methods: and . The method sends the HTTP GET request to The Trivia API with a query parameter, , that specifies that only 3 questions should be returned. Without the limit parameter, the API returns 10 questions by default. TriviaService GetTrivia ConvertTriviaToQuestions GetTrivia limit=3 The method converts the response from the API into an organized manner. The method also shuffles all question options randomly, so that a single option won’t be the answer to all questions. ConvertTriviaToQuestions To register and the HTTP client in your application's Dependency Injection (DI) container, modify as shown in the following code: TriviaService Program.cs using TwilioWhatsAppTriviaApp.Services; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddDistributedMemoryCache(); builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromSeconds(40); options.Cookie.IsEssential = true; }); builder.Services.AddHttpClient(); builder.Services.AddScoped<TriviaService>(); var app = builder.Build(); app.UseSession(); app.MapControllers(); app.Run(); Create the Trivia Controller Add an empty API controller class in a file named to the folder and modify its content as shown in the following code: TriviaController.cs Controllers using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using Twilio.AspNet.Core; using Twilio.TwiML; using Twilio.TwiML.Messaging; using TwilioWhatsAppTriviaApp.Models; using TwilioWhatsAppTriviaApp.Services; namespace WhatsappTrivia.Controllers; [Route("[controller]")] [ApiController] public class TriviaController : TwilioController { private const string SessionKeyIsGameOn = "IsGameOn"; private const string SessionKeyScore = "Score"; private const string SessionKeyCurrentQuestionIndex = "CurrentQuestionIndex"; private const string SessionKeyTotalQuestions = "TotalQuestions"; private const string SessionKeyQuestions = "Questions"; private static readonly string[] StartCommands = { "START", "S" }; private static readonly string[] OptionValues = { "A", "B", "C", "D" }; private readonly TriviaService triviaService; public TriviaController(TriviaService triviaService) { this.triviaService = triviaService; } [HttpPost] public async Task<IActionResult> Index() { var response = new MessagingResponse(); var form = await Request.ReadFormAsync(); var body = form["Body"].ToString().ToUpper().Trim(); await HttpContext.Session.LoadAsync(); var isGameOn = Convert.ToBoolean(HttpContext.Session.GetString(SessionKeyIsGameOn)); int currentQuestionIndex = HttpContext.Session.GetInt32(SessionKeyCurrentQuestionIndex) ?? 0; int totalQuestions = HttpContext.Session.GetInt32(SessionKeyTotalQuestions) ?? 0; if (StartCommands.Contains(body) && !isGameOn) { await StartGame(); HttpContext.Session.SetString(SessionKeyIsGameOn, "true"); response.Message(PresentQuestionWithOptions(currentQuestionIndex)); return TwiML(response); } if (OptionValues.Contains(body) && isGameOn) { var result = ProcessUserAnswer(body, currentQuestionIndex); response.Message(result); currentQuestionIndex++; if (currentQuestionIndex <= totalQuestions - 1) { HttpContext.Session.SetInt32(SessionKeyCurrentQuestionIndex, currentQuestionIndex); response.Append(new Message(PresentQuestionWithOptions(currentQuestionIndex))); } else { response.Append(new Message(EndTrivia())); } return TwiML(response); } response.Message(!isGameOn ? "*Hello! Send 'Start' or 'S' to play game*" : "*Invalid Input! Send a correct option 'A', 'B', 'C' or 'D'*"); return TwiML(response); } private async Task StartGame() { if (HttpContext.Session.GetString(SessionKeyQuestions) != null) { HttpContext.Session.Remove(SessionKeyQuestions); } var trivia = await this.triviaService.GetTrivia(); var questions = this.triviaService.ConvertTriviaToQuestions(trivia); AddNewQuestionsToSession(questions); HttpContext.Session.SetInt32(SessionKeyTotalQuestions, questions.Count); } private string ProcessUserAnswer(string userAnswer, int questionIndex) { bool optionIsCorrect = false; int score = HttpContext.Session.GetInt32(SessionKeyScore) ?? 0; var question = RetrieveQuestionFromSession(questionIndex); switch (userAnswer) { case "A": optionIsCorrect = question.Options[0].isCorrect; break; case "B": optionIsCorrect = question.Options[1].isCorrect; break; case "C": optionIsCorrect = question.Options[2].isCorrect; break; case "D": optionIsCorrect = question.Options[3].isCorrect; break; } if (optionIsCorrect) { score++; HttpContext.Session.SetInt32(SessionKeyScore, score); } return optionIsCorrect ? "_Correct ✅_" : $"_Incorrect ❌ Correct answer is {question.Options.Find(o => o.isCorrect).option.TrimEnd()}_"; } private string PresentQuestionWithOptions(int questionIndex) { var question = RetrieveQuestionFromSession(questionIndex); return $""" {questionIndex + 1}. {question.QuestionText} {OptionValues[0]}. {question.Options[0].option} {OptionValues[1]}. {question.Options[1].option} {OptionValues[2]}. {question.Options[2].option} {OptionValues[3]}. {question.Options[3].option} """; } private void AddNewQuestionsToSession(List<Question> questions) => HttpContext.Session.SetString(SessionKeyQuestions, JsonConvert.SerializeObject(questions)); private Question RetrieveQuestionFromSession(int questionIndex) { var questionsFromSession = HttpContext.Session.GetString(SessionKeyQuestions); return JsonConvert.DeserializeObject<List<Question>>(questionsFromSession)[questionIndex]; } private string EndTrivia() { var score = HttpContext.Session.GetInt32(SessionKeyScore) ?? 0; var totalQuestions = HttpContext.Session.GetInt32(SessionKeyTotalQuestions) ?? 0; var userResult = $""" Thanks for playing! 😊 You answered {score} out of {totalQuestions} questions correctly. To play again, send 'Start' or 'S' """; HttpContext.Session.Clear(); return userResult; } } This controller class is responsible for handling incoming messages, managing session state, and generating responses. It inherits from the class provided by the Twilio.AspNet.Core library, which gives you access to the method. You can use this method to respond with . The class uses methods to interact with the session. The valid inputs are elements in the and read-only arrays. The body of the incoming message is compared with these elements to ensure that the user sent a proper input, if not, a message will be sent to the user prompting them to make the right input based on the game's current state. Other fields with the “SessionKey” prefix are used to define private constant strings for session keys in the program. TwilioController TwiML TwiML, which is the Twilio Markup Language TriviaController HttpContext.Session StartCommands OptionValues The method is the main action method that handles incoming HTTP POST requests from WhatsApp via the route. It loads the session data using and retrieves data regarding the game state from the session using the and methods. Index /Trivia HttpContext.Session.LoadAsync() HttpContext.Session.GetString() HttpContext.Session.GetInt32() The use of underscores (_) and asterisks (*) at the beginning and end of certain strings is to achieve italic and bold text formatting respectively in rendered WhatsApp messages. Each helper method in the performs a specific task that supports the main functionality of the class. TriviaController method initializes the game by retrieving trivia questions, converting them into a format suitable for the game, and storing them in the session. StartGame method processes the user's answer to a question and determines if it is correct or not. ProcessUserAnswer method is responsible for formatting and presenting a question along with its options. PresentQuestionWithOptions method stores a list of questions in the session. It converts the questions to JSON format and saves the JSON string in the session. AddNewQuestionsToSession method retrieves a question from the session using the question index. RetrieveQuestionFromSession method generates a message to end the trivia game. This method also removes session data related to the game. Based on the configuration of session service in , this happens automatically when the session is idle for 40 seconds. EndTrivia Program.cs Test the Application To test the application, you need to set up Twilio Sandbox for WhatsApp, make your application endpoint publicly accessible and add the endpoint URL in the Sandbox configuration as a webhook. Setup Twilio Sandbox for WhatsApp Go to the , navigate to . Twilio Console Message > Try it out > Send a WhatsApp Message Follow the instructions on the page to connect to the sandbox, by sending a WhatsApp message from your device to the Twilio number provided in order to create a successful connection with the WhatsApp sandbox. Similarly, other individuals who wish to test your app with their respective numbers will need to follow the same procedure. Expose your webhook using ngrok for testing Now, open a shell terminal and run the following command to start ngrok and expose your local ASP.NET Core app by replacing with the full URL of your localhost you copied initially: <localhost-url> ngrok http <localhost-url> ngrok will generate a public URL that forwards requests to your local ASP.NET app. Look for the forwarding URL labelled in the ngrok terminal window and copy it. Forwarding Go back to the Twilio Try WhatsApp page, click on , and change the endpoint url with the URL generated by ngrok plus the route and ensure the method is set to POST. Then click Save to save the new sandbox configuration. Sandbox Settings When a message comes in forwarding /Trivia Project Demo Run your ASP.NET Core project using the following command: dotnet run Now, test your application by sending a message in the initial conversation with the Twilio Sandbox number. Conclusion By leveraging the power of the Twilio platform and WhatsApp, you have created an immersive trivia game experience for users to enjoy. You have also learned how to save and retrieve data from sessions. There are several ways this project can be improved upon. You can further improve this project by adding timer, handling exceptions, allowing users to choose difficulty and applying the chosen difficulty as a query parameter via The Trivia API URL (e.g ). Additionally, you can explore the possibility of creating a solution that allows users to fill out surveys via WhatsApp. https://the-trivia-api.com/api/questions?difficulty=hard