DEV Community

Cover image for Rainy with a chance of Azure cloud
Layla
Layla

Posted on • Edited on • Originally published at twilio.com

Rainy with a chance of Azure cloud

This article is part of #ServerlessSeptember. You'll find other helpful articles, detailed tutorials, and videos in this all-things-Serverless content collection. New articles are published every day — that's right, every day — from community members and cloud advocates in the month of September.

Find out more about how Microsoft Azure enables your Serverless functions at https://docs.microsoft.com/azure/azure-functions/.

We all have good days and bad days. On a good day, we may be filled with positive thoughts and on a bad day, we may forget that anything was ever positive!

alt gif of cartoon animal crying in the rain

In this post, we will use Twilio to text your positive thoughts and affirmations to an Azure Function and save them in Azure Table Storage. We will then create a timer-based function that will send a random happy thought from the table storage via SMS to your friends.

This post assumes some basic knowledge of C# and RESTful APIs.

To get started, we will need:

Create an Azure resource group and required resources

Once logged into your Azure portal, we will create a Resource Group, Functions App Service and Azure Function.

To create the Resource Group, go to "Resource groups" and then click the "plus" button to add a new resource. You will be taken to the "Create a new resource group" page where you can enter a name for your resource group, such as "RainyDays", choose a suitable region and then click "Review + create". Your resource will take a few moments to deploy.

alt azure portal create a resource group page image

Once deployed, we can create a new "Function App" resource. Click the "Create a resource" button on the top left of the side menu. In the search bar, search for and select "Function App", then click "Create".

alt azure portal create a function app image

A "Create Function App” dialogue will come up and you will need to give the app a unique name. Add it to the Resource Group that we just created. Then set the "Hosting Plan" to "Consumption Plan", choose an appropriate location, set "Runtime stack" to ".NET Core". There are a few other tabs on this wizard; "Hosting" and "Monitoring" being the more important ones. You can leave everything as the default. Finally, click the "Review and create" button at the bottom of the form.
After a few moments, you should have a new Function App Service, which you can then navigate to. In the centre of the screen will be a button called "+ New function". Click on this.

alt azure portal create a function image

The next screen will give several options, choose the IDE that you are using and if it's not there just choose the "Any editor + Core Tools" option.

alt azure portal choose a dev environment image

You should then be shown a very helpful "cheat sheet" with all the commands that you need to work with your project locally, using the CLI. Especially useful is the "Deploy your code to Azure" command, so you may want to make a note of that for later.
Now that we have the Azure side of everything set up, it's time to look at the code.

The Function App

Open up the solution that we cloned at the beginning of this post in your editor of choice. We will work on the master branch, the completed code is on the complete branch if you would rather see the finished solution.

We will focus on the code in src/Functions for this post.

Within this directory, we have two additional directories, Models and TableServices.
The Models directory contains TableConfiguration.cs and its interface for setting up the table storage dependency injection. It also has QuoteEntity.cs and Message.cs.

Table Storage structure and the "QuoteEntity" model

Table storage is a way to store NoSQL data in a structured way, with a schemaless design and a key/attribute store. For more information on how the data is structured, check out the docs here.

The model, QuoteEntity provided in the source code already, inherits from TableEntity. The overloaded constructor, public QuoteEntity(string phoneNumber, string quote), creates an object with all the properties needed to store it in a table store.

//"src/Functions/Models/QuoteEntity.cs"
public class QuoteEntity : TableEntity
{
   public QuoteEntity() {  }
   public QuoteEntity(string phoneNumber, string quote)
   {
       PartitionKey = "Quotes";
       RowKey = Guid.NewGuid().ToString();
       UserPhoneNumber = phoneNumber;
       Quote = quote;
   }
   public string UserPhoneNumber { get; set; }
   public string Quote { get; set; }
}

Enter fullscreen mode Exit fullscreen mode

Writing to an Azure table store

To avoid duplicating code, we will use TableDbContext to talk to the Azure Table Store. This code is in the "TableServices" directory and already provided. We will use dependency injection, recently added to Azure Functions natively, to inject the context into the constructor of both our functions. This DbContext has already been registered to the IoT container in the Startup.cs file as seen below:

//"src/Functions/Startup.cs"
ITableConfiguration tableConfig = new TableConfiguration
{
   ConnectionString = Environment.GetEnvironmentVariable("TableStorage-ConnectionString"),
   TableName = Environment.GetEnvironmentVariable("TableStorage-TableName")
};

builder.Services.AddSingleton(tableConfig);
builder.Services.AddSingleton<ITableDbContext, TableDbContext>();
Enter fullscreen mode Exit fullscreen mode

TableDbContext will handle all interactions with the table storage. It implements the ITableDbContext and has two private methods, CreateStorageAccountFromConnectionString, which ensures the table is created and GetRandom, which generates a random integer based upon the number of rows in a given collection.

Both the ILoggerFactory and the ITableConfiguration dependencies are injected into the class.

Let's implement the CreateTableAsync method first. We will use the private method CreateStorageAccountFromConnectionString() to validate the connection string and return a CloudStorageAccount object. We can then use this object to create a CloudTableClient object called tableClient which we can then use to create a table object, assuming that one doesn't exist already, and assign that to the CloudTable object called _table.

//"src/Functions/TableServices/TableDbContext.cs"
private CloudTable _table;
...
public async Task CreateTableAsync()
{
   // Retrieve storage account information from connection string.
   CloudStorageAccount storageAccount = CreateStorageAccountFromConnectionString();

   // Create a table client for interacting with the table service
   CloudTableClient tableClient = storageAccount.CreateCloudTableClient(new TableClientConfiguration());

   // Create the table
   _table = tableClient.GetTableReference(_tableConfiguration.TableName);

   if (await _table.CreateIfNotExistsAsync())
   {
       _log.LogInformation($"Created Table named: {_tableConfiguration.TableName}");
   }
   else
   {
       _log.LogInformation($"Table {_tableConfiguration.TableName} already exists");
   }
}
Enter fullscreen mode Exit fullscreen mode

Next, let's implement the method that will write data to the table, InsertOrMergeEntityAsync.

First, we must check if the table already exists, and if not, call the CreateTableAsync method that we previously created. Next, we check if the incoming TableEntity argument entity is null and throw an error if it is.

public  async Task<QuoteEntity> InsertOrMergeEntityAsync(TableEntity entity)
{
   if (_table is null)
   {
       await CreateTableAsync();
   }
   if (entity == null)
   {
       throw new ArgumentNullException(nameof(entity));
   }
try
{
}
 catch (Exception e)
   {
       _log.LogInformation(e.Message);
       throw;
}

}
Enter fullscreen mode Exit fullscreen mode

If entity is not null, then we move immediately into a "try/catch" statement. Add the following code to the try{}:

try
{

       // Create the InsertOrMerge table operation
       TableOperation insertOrMergeOperation = TableOperation.InsertOrMerge(entity);

       // Execute the operation.
       TableResult result = await _table.ExecuteAsync(insertOrMergeOperation);
       var insertedQuote = result.Result as QuoteEntity;

       // Get the request units consumed by the current operation. RequestCharge of a TableResult is only applied to Azure CosmoS DB
       if (result.RequestCharge.HasValue)
       {
           _log.LogInformation($"Request Charge of InsertOrMerge Operation:{ result.RequestCharge}");
       }

       return insertedQuote;
   }
  }
Enter fullscreen mode Exit fullscreen mode

In the above code, we attempt to use the TableOperation.InsertOrMerge(TableEntity entity) method provided in
Microsoft.Azure.Cosmos.Table library to create a table operation. We then execute the operation using _table.ExecuteAsync(TableOperation insertOrMergeOperation) method from the same library.

If all goes well, we are returned the created entity. If not we log any errors in the "catch".

The Inbound Function

Our first function, InboundFunction, is found in the "Functions" directory. It is an HTTP triggered function that we will invoke using a POST request from Twilio. The TableDbContext is injected into the constructor, so it is immediately available for us to use.

First, we will need to read the body of the request and write it to a string, so add the following line to the Run method:

//"src/Functions/InboundFunction.cs"
[FunctionName("InboundFunction")]
public async Task Run(
   [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)]
   HttpRequest req, ILogger log)
{
   string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
}

Enter fullscreen mode Exit fullscreen mode

We will need to extract the "From" number and the "Body" from the incoming request and map it to a Message model object. Message has a constructor available that will take the incoming request body as a string and map it for us.

string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
var message = new Message(requestBody);
Enter fullscreen mode Exit fullscreen mode

Now we can create a new QuoteEntity, using the overloaded constructor and the From and Body properties of the Message object. This can then be passed to the InsertOrMergeEntityAsync method on our DB context, _tableContext which will create and save our new quote:

string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
var message = new Message(requestBody);
var quoteEntity = new QuoteEntity(message.From, message.Body);
var newQuote = await _tableContext.InsertOrMergeEntityAsync(quoteEntity) as QuoteEntity;
Enter fullscreen mode Exit fullscreen mode

I would recommend adding some logging to the above code. If you would like to see how that's done, then check out the complete branch of the repo.

Writing to an Azure table store

We can now write data to our table store, but we also need to read it back out. Let's return to the TableDbContext and implement the remaining methods.

We want to be able to pick a random quote from our store and send it to all our friends. To do this we will need to bring back all the quotes as an array, create a random number based upon the length of our array, and then use that number to select a quote via its index.

But first, we need to check if the table exists!

//"src/Functions/TableServices/TableDbContext"
public async Task<QuoteEntity> GetRandomEntityAsync()
{
    if (_table is null)
    {
        await CreateTableAsync();
    }
    try { }
    catch (Exception e)
    {
        _log.LogInformation(e.Message);
        throw;
    }

}
Enter fullscreen mode Exit fullscreen mode

Once we are sure that we have a table created, we can then, within a "try/catch" statement, create a new table query TableQuery<TableEntity>, execute that query, and then create an array from the result.
As a precaution, if the array is empty, we return a default quote.
If it is populated, we then use the private GetRandom method to return a random number based upon the length of the array.
This random number will then be used to select a quote via its index, casting the result to a QuoteEntity and returning that value. If something goes wrong, we log it in the "catch".

try{
   var query = new TableQuery<TableEntity>();
   var quotes = _table.ExecuteQuery(query).ToArray();
   if (quotes.Length == 0)
   {
       return new QuoteEntity
       {
           Quote = "You look lovely today, text in and say something nice for your friends!"
       };
   }
   var random = GetRandom(quotes);
   var randomQuote = quotes[random] as QuoteEntity;
   return randomQuote;
}

Enter fullscreen mode Exit fullscreen mode

In addition to a random quote, we will also want a list of all the numbers that have texted a quote in. We can then use this list to send the random quote to all our friends, assuming your friends are sending in their happy thoughts to your app, too!

public async Task<IEnumerable<string>> ListNumbersAsync()
{
   if (_table is null)
   {
       await CreateTableAsync();
   }
}
Enter fullscreen mode Exit fullscreen mode

First, we again check if our table is created.
If it is, we can create our query from within a "try/catch" statement. We then create an empty list of type string, ready to populate with phone numbers.

try
{
   var list = new List<string>();
}
catch (Exception e)
{
   _log.LogInformation(e.Message);
   throw;
}
Enter fullscreen mode Exit fullscreen mode

Next, we will need to create a TableQuery object and execute this. Only this time, instead of creating an array, we will group by phone number, to ensure we only get each number once.

var list = new List<string>();
var query = new TableQuery<QuoteEntity>();
var quotes = _table.ExecuteQuery(query).GroupBy(e=>e.UserPhoneNumber);
Enter fullscreen mode Exit fullscreen mode

We can then iterate over our collection, adding the phone numbers to our newly created list:

var list = new List<string>();
var query = new TableQuery<QuoteEntity>();
var quotes = _table.ExecuteQuery(query).GroupBy(e=>e.UserPhoneNumber);
foreach (var quote in quotes)
{
   list.Add(quote.Key);
}

return list;
Enter fullscreen mode Exit fullscreen mode

The Outbound Function

Our second function, OutboundFunction, canalso found in the "Functions" directory. It is a function that is time-triggered based upon a schedule described by a cron expression. The TableDbContext is injected into the constructor and is available for us to use as a private attribute throughout the class.

We also fetch the Twilio phone number from settings, which we will set up soon, and assign it to the private attribute _phoneNumber, within the constructor.

Our Function cron expression is set to 0 0 10 * * *, which means it will fire at 10 am every day. If you would like it to fire differently, then this handy cheat sheet for cron expressions can help you.

We will be using Twilio to send our SMS messages. We will need to initialise the Twilio client within the Run method. The Twilio Account SID and Auth token will also be taken from settings.

//"src/Functions/OutboundFunction.cs"
public async Task Run([TimerTrigger("0 0 10 * * *")] TimerInfo myTimer, ILogger log)
{
   TwilioClient.Init(
       Environment.GetEnvironmentVariable("TWILIO_ACCOUNT_SID"),
       Environment.GetEnvironmentVariable("TWILIO_AUTH_TOKEN")
   );
}
Enter fullscreen mode Exit fullscreen mode

We can now use our _tableContext to first fetch the list of phone numbers and then the random quote we wish to send.

//collect all the telephone numbers of our users from the table storage
var numbers = await _tableContext.ListNumbersAsync();
//get a random quote from the database
var quoteEntity = await _tableContext.GetRandomEntityAsync();
Enter fullscreen mode Exit fullscreen mode

The final piece of this code, is to iterate over our number collection and send the quote to each user phone number.

foreach (var number in numbers)
{
       var message =  MessageResource.Create(
       from: new PhoneNumber(_phoneNumber),
       to: new PhoneNumber(number),
       body: quoteEntity.Quote );
}
Enter fullscreen mode Exit fullscreen mode

Setting the configuration

When developing locally, Azure functions rely on having all configuration within the local.settings.json file. You won't have this in your copy that you cloned from GitHub, as the .gitignore that comes with a function app excludes it. This is to ensure sensitive data isn't accidentally pushed to repositories. As a workaround, there should be a local.settings.demo.json file in the GitHub copy, so just rename that to be local.settings.json.

It is in this local.settings.json file where we will need to add all of the connection strings and secrets.

You can find the Table Storage connection string in the Azure portal.
Navigate to the Resource Group that we made earlier and click into the "storage account" which is used for our Table Storage. On the left-hand menu, click into "Access keys" and copy the "Connection string" listed against "key1".

This connection string will be the value applied to the following three keys in local.settings.json; "StorageConnectionSettings", "AzureWebJobsStorage" and "TableStorage-ConnectionString".

Your Twilio Account SID and Auth token can be found on the Twilio console. Grab your Twilio number and set it as a value against the setting "PhoneNumber".

We are now ready to test our functions!

Getting up and running

Start your functions from your IDE or from within the CLI with the following command from the following location "~/DailyAffirmations/src/Functions":

func start
Enter fullscreen mode Exit fullscreen mode

Your function will start and you will receive the following message in the console telling you about the HTTP trigger endpoints.

Http Functions:
    InboundFunction: [POST] http://localhost:7071/api/InboundFunction
Enter fullscreen mode Exit fullscreen mode

We will want to wire the Twilio number to this endpoint using ngrok.

Setting up ngrok

ngrok creates a public-facing URL that tunnels to our project running locally so we can test our endpoint without deploying to a server. Follow the instructions on the ngrok site to download and install it.

Once installed, run the following on your command line to start ngrok:

> ngrok http 7071 -host-header="localhost:7071"
Enter fullscreen mode Exit fullscreen mode

If your function isn't running on port 7071, then just update the above command with your port number.

You will then see an output similar to below.

alt ngrok in the console image

Copy the public-facing URL which should be along the lines of https://12345.ngrok.io.

To enable Twilio to make the initial request that fires off our string of events, we need to set up the webhook.

alt image of twilio console and where to set up webhook

Go to the Twilio console and find the number that you added for this project. Next, add the API endpoint https://<NGROK_NUMBER>.ngrok.io/api/Inbound Function from ngrok and paste into the A MESSAGE COMES IN section, then save.
Now you can send an SMS with your inspiring thought as the message and it will be written to the table store.

alt gif with inspirational affirmations

Testing the timer trigger

We don't want to wait for our trigger to fire in order to test it… that only happens once a day! It's much more convenient to test it with an HTTP POST request.
For the purpose of testing, we’ll use Postman to create a new POST request to the following URL "http://localhost:7071/admin/functions/OutboundFunction", with "Content-Type" set to "application/json" and the body set to an empty JSON object {}.
Once you send this post you will receive an SMS with a happy thought!

Publishing to Azure
An easy way to publish your app is by using the Azure CLI. First, make sure you are logged into your Azure account with the command az login, which will open a browser and enable you to log in to Azure.
Then, from within "src/Functions" run the command we made a note of at the beginning:

func azure functionapp publish <FUNCTION_APP_NAME>
Enter fullscreen mode Exit fullscreen mode

Don't forget to add your app settings and update your Twilio endpoint to your Function app's URL.

What next?

We've seen how we can quickly receive an incoming SMS message and send out an email. A great update to the above code would be to add further logging and error handling. You could also add a third function that enables you to add your friend's numbers without them having to text a quote in.

Let me know what other cool ideas you have for Twilio SMS and Azure Functions. I can't wait to see what you build!

Top comments (0)