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
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__.
To complete this tutorial, you will need:
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 --no-openapi
in the command above.
Change into the project directory by running this command:
cd TwilioWhatsAppTriviaApp
Install the
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 Controllers folder, remove the boilerplate template controller file, WeatherForecastController.cs, and also remove the WeatherForcast.cs in the project directory.
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.
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 modifyingProgram.cs, as shown in the following code:
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();
AddDistributedMemoryCache()
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.
AddSession()
registers the session services, enabling the application to maintain session state. The options
parameter allows you to configure various session-related options. IdleTimeout
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. Cookie.IsEssential
ensures that the session’s state remains functional and even in scenarios where cookie rejection is enabled.
Session support is enabled by adding the UseSession
middleware to the application pipeline, that is, your application gains access to a session object that can be used to store and retrieve data.
Create a new folder, Models in your project directory. Add two model class files, TriviaApiResponse.cs and Question.cs with properties as shown in the following code samples:
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 TriviaApiResponse
model includes properties that represent the fields of The Trivia API response. The JsonProperty
attribute ensures that each property is correctly populated with the corresponding JSON data.
For a streamlined way to handle trivia questions, the Question
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.
Create a Services folder in your project directory and add a new class file named TriviaService.cs. Modify its contents, as shown in the following code:
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;
}
}
TheTriviaService
class contains two methods: GetTrivia
and ConvertTriviaToQuestions
. The GetTrivia
method sends the HTTP GET request to The Trivia API with a query parameter, limit=3
, that specifies that only 3 questions should be returned. Without the limit parameter, the API returns 10 questions by default.
The ConvertTriviaToQuestions
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.
To registerTriviaService
and the HTTP client in your application's Dependency Injection (DI) container, modify Program.cs as shown in the following code:
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();
Add an empty API controller class in a file named TriviaController.cs to the Controllers folder and modify its content as shown in the following code:
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 TwilioController
class provided by the Twilio.AspNet.Core library, which gives you access to the TwiML
method. You can use this method to respond with TriviaController
class uses HttpContext.Session
methods to interact with the session.
The valid inputs are elements in theStartCommands
and OptionValues
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.
TheIndex
method is the main action method that handles incoming HTTP POST requests from WhatsApp via the /Trivia route. It loads the session data using HttpContext.Session.LoadAsync()
and retrieves data regarding the game state from the session using the HttpContext.Session.GetString()
and HttpContext.Session.GetInt32()
methods.
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 TriviaController
performs a specific task that supports the main functionality of the class.
StartGame
method initializes the game by retrieving trivia questions, converting them into a format suitable for the game, and storing them in the session.ProcessUserAnswer
method processes the user's answer to a question and determines if it is correct or not.PresentQuestionWithOptions
method is responsible for formatting and presenting a question along with its options.AddNewQuestionsToSession
method stores a list of questions in the session. It converts the questions to JSON format and saves the JSON string in the session.RetrieveQuestionFromSession
method retrieves a question from the session using the question index.EndTrivia
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 Program.cs, this happens automatically when the session is idle for 40 seconds.
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.
Go to the
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.
Now, open a shell terminal and run the following command to start ngrok and expose your local ASP.NET Core app by replacing <localhost-url>
with the full URL of your localhost you copied initially:
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 Forwarding in the ngrok terminal window and copy it.
Go back to the Twilio Try WhatsApp page, click on Sandbox Settings, and change the When a message comes in endpoint url with the forwarding URL generated by ngrok plus the /Trivia route and ensure the method is set to POST. Then click Save to save the new sandbox configuration.
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.
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