Serverless

Chocolate, Durable Entities, and Seasons of Serverless

January 4, 2021

Bienvenue en France for this week's Seasons of Serverless challenge.

In this article, I'll cover my solution to Challenge 6: The Magic Chocolate Box!

Table of Contents

Prerequisites

1. Challenge outline

2. My solution

3. Check the results

4. Test it yourself!

5. More resources


Prerequisites

  • Visual Studio Code
  • Azure Functions Extension Installed
  • An Azure account

1. Challenge outline

In this challenge, our goal is to develop a solution that would allow family members to reserve their favorite chocolates from a chocolate box. We need to be able to see a list of available chocolates and update the list when reservations are made.

We will use Durable Entities for our solution.

2. My solution

My solution calls for three parts: one Durable Entity and two Azure Functions.

I used JavaScript for my solution but there are lots of different languages you can use.

The Durable Entity: Chocolate

When creating my Durable Entity in Visual Studio, I selected the Durable Functions activity and named the function Chocolate.

This will create two files: function.json and index.js.

In the function.json file, I adjusted the type from activityTrigger to entityTrigger:

// Chocolate/function.json

{
  "bindings": [
    {
      "name": "context",
      "type": "entityTrigger",
      "direction": "in"
    }
  ]
}

In the index.js file, I added the following code:

// Chocolate/index.js

const df = require("durable-functions");

const ChocolateBox = [
    "Dark Sea Salt Caramel",
    "Milk Sea Salt Caramel",
    "Apple Cider Caramel",
    "Almond Butter Crunch",
    "Cherry Cordial",
    "Coconut Delight",
    "Dipped Pineapple",
    "Dipped Apricot",
    "Dipped Orange Peel",
    "Espresso Shot",
    "Peanut Butter Chocolate",
    "Raspberry Vienna",
    "Almond Caramel Cluster",
    "Macadamia Caramel Cluster",
    "Pecan Caramel Cluster",
    "Pretzel Caramel Cluster"
];

let Chocolates = {
    available: ChocolateBox,
    reserved: {}
};

module.exports = df.entity(function(context) {
    const currentValue = context.df.getState(() => 0);
    switch (context.df.operationName) {
        case "add":
            const {name, item} = context.df.getInput();
            if(currentValue.available.includes(item)) {
                currentValue.available.splice(currentValue.available.indexOf(item), 1);
                if (currentValue.reserved[name]) {
                    currentValue.reserved[name].push(item);
                } else {
                    currentValue.reserved[name] = [ item ];
                }
            }
            context.df.setState(currentValue);
            break;
        case "reset":
            context.df.setState(Chocolates);
            break;
        case "get":
            context.df.return(currentValue);
            break;
    }
});

This code can be logically divided into three parts:

  1. A ChocolateBox which contains an array of chocolates. You can adjust it to reflect your personal chocolate box.
  2. A Chocolates object which lists the available chocolates and will update to show which chocolates are reserved.
  3. An aggregator (stateful entity) to either add, get, or reset the chocolate box.

Azure Function #1: GetReservations

This function will get a list of the available chocolates and reservations made. When creating my first Azure Function in Visual Studio, I selected the HTTP trigger, named the function GetReservations, snd gave it an Anonymous authorization level.

This will create two files: function.json and index.js.

In the function.json file, I adjusted the code to this:

// GetReservations/function.json

{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    },
    {
      "name": "chocolates",
      "type": "durableClient",
      "direction": "in"
    }
  ]
}

In the index.js file, I added the following code:

// GetReservations/index.js

const df = require("durable-functions");

module.exports = async function (context) {
    const client = df.getClient(context);
    const entityId = new df.EntityId("Chocolate", "myChocolate");
    const stateResponse = await client.readEntityState(entityId);

    context.res = {
        // status: 200, /* Defaults to 200 */
        body: stateResponse.entityState
    };
};

Azure Function #2: ReserveChocolate

This function will allow us to reserve chocolates by listing a name and chocolate type. When creating my second Azure Function in Visual Studio, I selected the HTTP trigger, named the function ReserveChocolate, snd gave it an Anonymous authorization level.

This will create two files: function.json and index.js.

In the function.json file, I adjusted the code to this:

// ReserveChocolate/function.json

{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get",
        "post",
        "delete"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    },
    {
      "name": "chocolates",
      "type": "durableClient",
      "direction": "in"
    }
  ]
}

In the index.js file, I added the following code:

// ReserveChocolate/index.js

const df = require("durable-functions");

module.exports = async function (context, req) {
    try {
        const name = (req.query.name || (req.body && req.body.name));
        const item = (req.query.item || (req.body && req.body.item));
        const reset = (req.query.reset || (req.body && req.body.reset));

        const client = df.getClient(context);
        const entityId = new df.EntityId("Chocolate", "myChocolate");
        let responseMessage = "";

        if (req.method === "DELETE" || reset) {
            await client.signalEntity(entityId, "reset")
            responseMessage = "All reservations have been cleared.";
        } else {
            if (!name || !item) {
                context.res = {
                    status: 400,
                    body: "name and item parameters are required"
                };
                return;
            }
            await client.signalEntity(entityId, "add", {name, item});
            responseMessage = "Updated reservations";
        }

        context.res = {
            // status: 200, /* Defaults to 200 */
            body: responseMessage
        };
    } catch (error) {
        context.res = {
            status: 500,
            body: { message:"error", error }
        };
    }
};

This code accepts three values: a name, an item, and a reset.

When someone types in a name this will be the person reserving chocolate. When someone types in an item, this will be the name of the chocolate they wish to reserve.

Finally, if someone sets reset to True, the chocolate box is reset and all reservations are erased.

3. Check the results

My solution follows three steps:

Step 1: Make a reservation with ReserveChocolate

Input

Chocolate reservation input

Output

Chocolate reservation output

Step 2: See updated chocolates and reservations with GetReservations

Input

Get chocolate reservation input

Output

Get chocolate reservation output

Step 3: Clear all reservations with ReserveChocolate

Input

Clear reservations input

Output

Clear reservations output

4. Test it yourself!

You can test it out with the following links. I currently have it assigning an Espresso Shot Chocolate to myself, but you can edit the URL to assign chocolates to more people:

  1. View chocolate box
  2. Make a reservation
  3. View updated chocolate box and reservations
  4. Clear chocolate box

5. More resources

  1. Join this week's Seasons of Serverless challenge
  2. Learn more about Azure Functions on Microsoft Learn
  3. Check out my solution to Challenge 5
Copyright © Locksley Kolakowski 2021 | All opinions are my own