Background
For someone who runs a static website without a WebApp server or virtual machine, this can add a lot of additional functionality that you would normally need dedicated infrastructure to provide. The other good thing is Azure Functions are priced on execution time and are cheap compared to running a WebApp server.
For example, having a jQuery contact form rather than publishing an e-mail address would normally require a full web application to submit the form. Instead of converting my site back to a web application and moving away from a CDN delivered site, let’s take a look at building a Contact Form API using Azure Functions.
Getting Started
It’s important to note that Microsoft has some built in tooling in the Azure portal that allows you to create functions directly within the portal without any editor in a variety of languages. Through this article, however, I will be using C# in Visual Studio Community Edition as I am more comfortable with that IDE and it allows me to test and debug locally before deploying the function to Azure.
To follow along with this article, you will also need a SendGrid
account and an API key generated for your function to use. You can start with Full Access to the SendGrid
APIs to start with and restrict it down later as you move into production. It is also beneficial to install the Azure SDK and Azure Storage Emulator so you can test locally before moving onto Azure resources.
Step 1 - Create a Project
Firstly, create your Azure Functions project in Visual Studio. Remember Functions should be constructed with a micro-services mind set, so when planning Functions, the project you create here should be for a group of functions. In my case, I’ve created a functions project to hold all the functions I will need for my website.
When you create a new functions project, you will also need to select the version you are creating (in this case, version 2 using .NET Core) as well as the storage account the function uses and its access rights. At the moment, we are selecting the Storage Emulator to test locally, however there is an environment-based variable that holds this setting for when we deploy to Azure. The trigger at this point is the trigger created for a default function and is not important at this stage.
When the project has been created, you should have only a few files. If you selected HTTP Trigger, remove the Function1.cs file as it is the default Function and we are going to create one from scratch.
Step 2 - Create the Function
I added a new folder to my project called “Contact Form” and added a new Function call ContactPostMessage.cs. All this function will do is accept POST
messages from the internet with a specific JSON payload and send them as an e-mail.
This Function should run off a “Http trigger” and have “Function” access rights applied to it. This basically means that anyone who has the Function access key can use HTTP / HTTPS to send data to this API which will convert it to an email. The reason we are using Function as the access rights is we will be putting an API Management front end to wrap any Http trigger Functions together and provide some additional functionality.
Step 3 - Code the Function
Firstly, let's create a function.json file to provide the bindings for the input and output of the function. This file must sit in the same directory as your function and is an array of bindings in JSON format. The code for this file looks like this:
{
"bindings": [
{
"authLevel": "function",
"name": "req",
"type": "httpTrigger",
"direction": "in",
"methods": [
"post"
]
},
{
"name": "$return",
"type": "http",
"direction": "out"
},
{
"name": "mail",
"type": "sendGrid",
"apiKey": "SENDGRID_API_KEY",
"to": "CONTACT_TO_ADDRESS",
"direction": "out"
}
]
}
This file contains one input binding, the httpTrigger
type with only post allowed, and two output types, a http return type and SendGrid
output type. Now let’s modify the function to accept, check and process a JSON file with the information someone will enter to send us a message. Let’s step through the ContactPostMessage
class to show what we are doing with this function:
public static class ContactPostMessage
{
[FunctionName("ContactPostMessage")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
[SendGrid(ApiKey = "AzureWebJobsSendGridApiKey")]
IAsyncCollector<SendGridMessage> messageCollector,
ILogger log)
{
The first few lines define the class and the run function, which is called when the function is triggered. This matches what we have in the function.json file. Firstly, we are processing an HttpTrigger
and trapping the request in the req
variable. We are also injecting a SendGrid
Message collector in the variable messageCollector
and the logging system into the variable log.
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
string name, email, subject, message = "";
try
{
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = data?.name;
email = data?.email;
subject = data?.subject;
message = data?.message;
}
catch(Exception ex)
{
log.LogInformation("Error trying to transform JSON payload - " + ex.ToString());
return new BadRequestObjectResult("Malformed JSON payload");
}
In this next segment of code, we are reading the body of the request and trying to unpack a JSON object that contains the data of the contact form. We use a stream reader to get the request body, then the JsonConvert
class of the Newtonsoft.Json
library to unpack the body as a JSON object. We then put the required data into some string
variables. We do this in a try
-catch
method so we can send any errors back if the body of the message doesn’t meet our basic requirements.
if (Regex.IsMatch(name, @"^[A-Za-z]{3,}$"))
return new BadRequestObjectResult("Name may only contain alpha-numeric characters");
if (Regex.IsMatch(subject, @"^[<>%\$!#^&*+=|/`~]$"))
return new BadRequestObjectResult("Subject may not contain special characters");
if (Regex.IsMatch(message, @"^[<>%\$!#^&*+=|/`~]$"))
return new BadRequestObjectResult("Message may not contain special characters");
try
{
var fromAddr = new System.Net.Mail.MailAddress(email);
if (fromAddr.Address != email)
return new BadRequestObjectResult("E-Mail address is not valid");
}
catch
{
return new BadRequestObjectResult("E-Mail address is not valid");
}
Once we have done the basic checks, we use Regex to do a more complete check of the data elements themselves, making sure we have a valid email address, message, subject and name.
var mail = new SendGridMessage();
mail.AddTo(Environment.GetEnvironmentVariable("CONTACT_TO_ADDRESS",
EnvironmentVariableTarget.Process));
mail.SetFrom(email, name);
mail.SetSubject(subject);
mail.AddContent("text/html", message);
await messageCollector.AddAsync(mail);
return new OkResult();
}
}
Finally, we compose the JSON payload into a SendGrid
message and send it using the injected collector. Once this is all done, deploy the functions project to Azure, I use Azure DevOps to publish connected to my repository. For the code to work, you need the following variables added to your published resource (in the configuration section of the platform features area):
AzureWebJobsSendGridApiKey
- Your SendGrid
API Key AzureWebJobsStorage
- The Storage account to associate with this function app CONTACT_TO_ADDRESS
- The to address to send all generated e-mails to
Step 4 - Present the Function using API Management
You can just publish a function as is, however I put mine behind an API Management gateway so I can bundle any further functions up into a set of products, but it also allows me to specify advanced policies, such as rate limits, to help protect from people spamming the API.
API Management has an option to allow you to link directly with an Azure function and it will set up the correct access mechanisms in the background. Once this is setup, you can publish a number of functions through a single front end.
Summary
So that is the basics of creating an Azure Function. Quite useful for implementing fire and forget workloads. You can find the example code for this function as a GitHub Project.
History
- 16th July, 2019: Initial version