The goal of this lesson is to create a solution that combines multiple functions to build a scalable event-driven application. You will need to build most parts yourself based on the requirements and instructions given.
📝 Tip - If you're stuck at any point you can have a look at the source code in this repository.
This lesson consists of the following exercises:
Nr | Exercise |
---|---|
1 | Receiving game scores over HTTP |
2 | Publish score received events to queue |
3 | Calculate high scores from events |
4 | Build Web API to retrieve high score list |
5 | Notify HTML client of updates |
The architecture of the solution you are going to create in these exercises looks as follows:
It consists of three Azure Functions in a single Web Application.
In this exercise, you will build the first function ReceiveGameScoresFunction
that is triggered by a POST HTTP request containing a JSON payload.
-
Create a new Visual Studio 2019 or Visual Studio Code solution
AzureFunctionsWorkshop
with an Azure Functions projectRetroGamingFunctionApp
. -
Add a function
ReceiveGameScoresFunction
with an asyncRun
method to the project. The function should be triggered by a POST request with a JSON payload containing an array of game scores:[ { "Nickname" : "John Doe", "Points" : 42, "Game" : "Pacman" }, { "Nickname" : "Jane Doe", "Points" : 1337, "Game" : "Pacman" } ]
-
Create a class
GameScore
that represents the data from the HTTP request and place it in a new folderModels
.public class GameScore { public int Points { get; set; } public string Game { get; set; } public string Nickname { get; set; } }
-
Deserialize the body of the HTTP request to
IEnumerable<GameScore>
objects using theJsonConvert.DeserializeObject
method. -
Return an
OkObjectResult
object from the function. -
Build, run and test your function using the assignment.http file from the
tst
folder in the root of the repository.
Next, you will expand the function ReceiveGameScoresFunction
to send messages for each of the received game scores to a storage queue as a way to broadcast an event. By splitting the receiving of game scores from the calculation of high scores, the scalability of the solution will improve.
-
Start by including a reference to the NuGet package
Microsoft.Azure.WebJobs.Extensions.Storage
, as you are going to use storage queues and tables. -
Create a class to hold the event data that is going to be put in the message for the queue. Place this in the
Models
folder as well.public class GameScoreReceivedEvent { public Guid Id { get; set; } public GameScore Score { get; set; } }
-
Change the signature of the function
ReceiveGameScoresFunction
to include an output binding to a storage queue namedgamescorequeue
.📝 Tip - Since there is more than one message to put on the queue, use the
ICollector<GameScoreReceivedEvent>
type for the output binding. 📝 Tip - Make sure to create the queue in the Azure Storage Explorer before running the function. -
Add code to your function implementation right after deserializing the array of game scores. Iterate over the received
GameScore
objects and add a newGameScoreReceivedEvent
object to the collector. The object should contain a newly generated GUID and the currentGameScore
object. -
Build, run (
func host start
) and test your application again. Use the Azure Storage Explorer to check that indeed two messages where placed in the queuegamescorequeue
.
Every message on the gamescorequeue
indicates a new game score that might potentially be a new high score for a certain game. In this exercise you will create a new Azure Function to trigger on new messages in the queue and calculate the new high score for the player of that game.
The high scores are stored in a storage table HighScores
. When a message arrives, the table is queried for a previous score by the player. If the received score is better than the one stored, you will update the table entry to reflect the higher score. Should there be no previous score in the table, a new high score entry is inserted.
-
Create a new Azure Function
CalculateHighScoreFunction
that is triggered by a message in thegamescorequeue
. The message will contain theGameScoreReceivedEvent
object used in the previous exercise. Also, the method signature forRun
should perform input and output binding to aTableClient
to read from and write to the storage tableHighScores
.📝 Tip - Make sure to import the proper namespace for the
TableClient
type. It should beAzure.Data.Tables
for the latest Azure Storage SDK. -
Create a class to represent a high score:
public class HighScoreEntry : ITableEntity { public int Points { get; set; } public string PartitionKey { get; set; } public string RowKey { get; set; } public DateTimeOffset? Timestamp { get; set; } public ETag ETag { get; set; } }
🔎 Observation - As you can see there is only one property needed, the Points. The
PartitionKey
is used for the game name and theRowKey
is for the player nickname. TheTimestamp
andETag
properties are used by the storage table to keep track of changes. -
Implement the function to query the table for an existing high score entry. Here are some useful fragments to help you out.
try
{
var result = await table.GetEntityAsync(message.Score.Game.ToLower(), message.Score.Nickname);
entry = result.Value;
}
catch (RequestFailedException e) // item does not exist
{
entry = new HighScoreEntry
{
PartitionKey = message.Score.Game.ToLower(),
RowKey = message.Score.Nickname
};
}
```
-
Check whether the retrieved result contains a
HighScoreEntry
instance by validating that you did not get a 404 result. If there was no previous score stored, create a new objectHighScoreEntry
with thePartitionKey
andRowKey
set to the game name and player's nickname from the event and a zeroPoints
score. -
When the score from the received event is better than a previously registered score, or it is the first score for that player, we have a new high score. Update the
Points
for theHighScoreEntry
and store it in the tableHighScores
. The following code fragment stores an entry in a storage table:entry.ETag = ETag.All; entry.Points = message.Score.Points; var store = await table.UpsertEntityAsync(entry);
-
Build and run your solution and test your new function. Verify that the entries are stored in the storage table using the Azure Storage Explorer. Change the points in the game scores to see the effect in behavior of the function.
In this exercise you will create a REST Web API by building a third Azure Function that is triggered by an HTTP GET request. Also, you are going to consume this REST API with a client HTML application that is included in the sources of the assignment exercise.
-
Add a new Azure Function
RetrieveHighScoreListFunction
to your project. It should trigger on HTTP GET requests for a route with pathhighscore/{game}
. The placeholder{game}
represents the name of the game. It is used to retrieve the correct high score list.📝 Tip - Remember that for HTTP triggers you can include the names of placeholders in routes as arguments to your
Run
method, so you can easily use the value. -
Check the query string value for
top
of the HTTP request that triggered your function. It is optional and, if present, contains the number of entries that should be returned for the high score list. It should not exceed 20 and be 10 by default. -
Add an input binding argument
table
for the storage table to the signature of theRun
method. Query theHighScores
table using thisTableClient
reference.Pageable<HighScoreEntry> queryResults = table.Query<HighScoreEntry>(x=>x.PartitionKey == game, top );
-
Return an
OkObjectResult
that holds a projected array of anonymous typed objects for the nickname and points for each player in the high score list. The anonymous type should be like this:return new OkObjectResult(queryResults.Take(top).Select(e => new { Nickname = e.RowKey, Points = e.Points }));
-
Build, run (
func host start
) and test your new Azure Function. Make an HTTP GET request tohttp://localhost:7071/api/highscore/pacman
or any of the other game names you might have used. Fix any errors if needed. -
Copy the entire project
RetroGamingWebsite
from the source code to the root of your solution. Inspect thewwwroot
folder and theindex.html
file in particular. It is a single HTML file that uses Vue.js to build a user interface with the high score list. Find the JavaScript code that calls the Web API you build in the previous steps of this exercise. -
Start both the Azure Function project and the new Web API project. In Visual Studio you can do this using multiple startup projects. Navigate to the
/index.html
page to verify a working website consuming your Web API. If it is not working, CORS settings of your HTTP trigger function might be the culprit. Change thelocal.settings.json
file to include a new root level section forHost
, like so:"Host": { "LocalHttpPort": 7071, "CORS": "*" }
Note: This final exercise is optional. It requires an Azure subscription to use Azure SignalR Service.
This exercise uses Azure SignalR Service to push updates of the highscore list to the active browser clients running the HTML website from the previous exercise. Whenever a new highscore is detected during the calculation, a message is pushed to a SignalR hub. You are going to use Azure SignalR Service to host the hub.
-
Include a reference to the NuGet package
Microsoft.Azure.WebJobs.Extensions.SignalRService
to your Azure Functions project. -
Revisit the
CalculateHighScoreFunction
and add an output binding for a SignalR hub namedleaderboardhub
.[SignalR(HubName = "leaderboardhub")] IAsyncCollector<SignalRMessage> signalRMessages
-
Right after a new high score is stored, also add a
SignalRMessage
object to the async collector. Since the JavaScript in the HTML page already assumes some values for the target in the hub, this fragment might be of use:await signalRMessages.AddAsync(new SignalRMessage() { Target = "leaderboardUpdated", Arguments = new string[] { }, }); await signalRMessages.FlushAsync();
-
Create a new Azure SignalR Service in your Azure subscription. Retrieve the connection string from the Azure Portal and include it in the
local.settings.json
file in the root of your solution underValues
for a newAzureSignalRConnectionString
key.{ ... "Values": { ... "AzureSignalRConnectionString": "your-connection-string", }, ... }
-
Build, run and test your entire solution again. Post a new high score for the game
Pacman
and watch any changes in the high score list appear. Again, fix any errors.📝 Tip - Remember you can always compare your implementation with the complete lab files for the assignment in the repository.
If you have an Azure subscription you may want to explore some more. Here are some ideas for additional tasks in this solution:
- Use an Azure storage table and queue instead of using the storage emulator. This will require you to create the resources in Azure, find the connection strings. Change the
local.settings.json file
and replaceUseDevelopmentStorage=true
with the actual connection string to the Azure resource. - Change the implementation for
ReceiveGameScoresFunction
andCalculateHighScoreFunction
to use a Cosmos DB resource in Azure or using the Azure Cosmos DB emulator
You have created an entire application using multiple Azure Functions. With this scalable, event-driven as a basis you can start to discover more of the use of Azure Functions in your implementations.