Post HTML forms to Cosmos DB with Azure Functions (for free)

Introduction

I love Cosmos DB and Azure Functions and have been using them a lot in various projects.

One thing that seemed to put off people from using Cosmos DB was the price tag. However, recently Microsoft announced a free tier, making it a great storage option for projects of any size.

In this post I’ll take you through a fun scenario: using Azure Functions to post HTML form values to Cosmos DB. Why is it fun? Because it’s easy and it doesn’t cost anything 🙂

To follow these steps, you should have basic knowledge of HTML, Cosmos DB and Azure functions. To learn more, start here:

By the way, I’m using the new Azure Functions management experience for the screenshots below, but of course the scenario described will work with the older UI as well.

The HTML form

Assume we have a simple HTML form as shown here:

The markup is as follows. I’m using Bootstrap in this example, to have a bit of styling, but this isn’t required. Any valid HTML form will do.

There are three inputs I want to store: firstName, lastName and email.

Notice the post value in the method attribute. This tells the browser to post the form values to the URL in the action attribute. That URL points to an HTTP triggered Azure Function called NewsletterHttpPostTrigger.

<form method="post" action="https://newsletter-func-app.azurewebsites.net/api/NewsletterHttpPostTrigger">
  <div class="form-group">
    <label for="firstName">First name</label>
    <input type="text" class="form-control" id="firstName" name="firstName" placeholder="Enter first name" required="required" />
  </div>

  <div class="form-group">
    <label for="lastName">Last name</label>
    <input type="text" class="form-control" id="lastName" name="lastName" placeholder="Enter last name" required="required" />
  </div>

  <div class="form-group">
    <label for="email">Email</label>
    <input type="email" class="form-control" id="email" name="email" placeholder="Enter email" required="required" />
  </div>

  <button type="submit" class="btn btn-primary btn-block">Sign up</button>
</form

Setting up the Cosmos DB account

Over to the Azure Portal.

Creating a Cosmos DB account only takes a minute. I have added one with the following settings. A free tier gives you 400 RU/s and 5 GB of storage; more than enough to store a lot of form data.

That’s all it takes. We can just leave the account empty, as the database and collection that will store the data will be created automatically through a binding on the Azure Function.

Adding and configuring the Azure Function

OK. We have our form and storage account in place, so let’s move on to the Azure Function.

I’ve added one with the following settings, using .NET Core 3.1 for the runtime stack:

In the hosting properties, I’ve made sure to choose Windows for the OS and App service plan for the plan. Notice that I have picked the Free F1 SKU. This has limited memory and runs on a shared infrastructure which allows for 60 minutes of free CPU processing per day, which is sufficient for our scenario:

To handle the POST action for the form, we need an HTTP trigger function. So I added a new function as shown below:

I also set the authorization level to anonymous, so the HTML form can reach it. The drawback is that this makes the function available to anyone. I’ll show you how to configure security settings further on in this article.

I want the function to parse the HTML form values and save them to Cosmos DB with the least amount of effort. So I will add a binding between the function and my Cosmos DB account to do the heavy lifting for me.

With the function created, I need to open Integration in the left panel:

This takes me to a graphical view of the function flow, where I first open the trigger properties:

I want to make sure that POST is the only HTTP method allowed, since that’s the only one we need for the HTML form:

OK. Now I can add the Cosmos DB binding under Outputs:

The output properties are shown below. I’ve chosen my Cosmos DB account in the connection setting and filled in a name for the parameter (this is used in the Azure Function code).

I’ve also set a database name and collection name and enabled auto creation to make sure they are added to the Cosmos DB account if they’re missing.

A partition key is used to logically organize containers across partitions for performance reasons. All data with the same partition key goes to the same partition. You don’t have to use it in this case, but I like to always explicitly use a property called partitionKey, to keep things clear.

With all this configured, the Integration view now looks like this:

At this point, we have our inputs and outputs lined up, so all that’s left to do, is add a bit of code to parse the HTML form data.

So let’s go to the function’s code editor and look at the function arguments first:

public static async Task<IActionResult> Run(HttpRequest req, ILogger log, IAsyncCollector<dynamic> outputDocuments)
{
  var requestUtc = DateTime.UtcNow;
  ...
}

The incoming form data will be available via req. This maps to the HTTP trigger we just looked at.

The output is mapped to outputDocuments, which is a collection wrapper of type IAsyncCollector<dynamic>. IAsyncCollector is a generic interface, so you could specify a specific model type for your document. Dynamic is fine in our use-case, because it allows us to add any property from the HTML form data easily. Anything the function puts in outputDocuments (which can be multiple items) will be automatically stored in the Cosmos DB database and collection that we configured in the output binding.

Data posted from an HTML form comes in as a query string containing a series of key-value pairs. Fortunately, there’s a handy utility class in the System.Web namespace, which has a ParseQueryString function.

So I read the form values and parse them as shown below. The resulting formValues will contain a name-value collection with each key-value pair from the query string.

string requestBody = null;
using (var bodyReader = new StreamReader(req.Body))
{
    requestBody = await bodyReader.ReadToEndAsync();
}

var formValues = System.Web.HttpUtility.ParseQueryString(requestBody);

Now all that’s left to do is to create a dynamic object to represent the output document.

Since it’s dynamic, I can add any property I want. I’ll start with the timestamp and the partition key property. For this, I’ll use part of the date, so a new partition is created for each month.

Next I add the values from the HTML form, by getting them from the parsed name-value collection.

The dynamic output object is then added to the async collector:

var outputDocument = new 
{
    requestUtc,
    partitionKey = requestUtc.ToString("yyyy-MM"),
    firstName = formValues["firstName"],
    lastName = formValues["lastName"],
    email = formValues["email"]
};

await outputDocuments.AddAsync(outputDocument);

So that’s all the code we need in the function. The complete code is as follows:

#r "Newtonsoft.Json"

using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using Microsoft.Azure.WebJobs;

public static async Task<IActionResult> Run(HttpRequest req, ILogger log, IAsyncCollector<dynamic> outputDocuments)
{
    var requestUtc = DateTime.UtcNow;

    string requestBody = null;
    using (var bodyReader = new StreamReader(req.Body))
    {
        requestBody = await bodyReader.ReadToEndAsync();
    }

    log.LogInformation($"Request body: {requestBody}");

    var formValues = System.Web.HttpUtility.ParseQueryString(requestBody);

    var outputDocument = new 
    {
        requestUtc,
        partitionKey = requestUtc.ToString("yyyy-MM"),
        firstName = formValues["firstName"],
        lastName = formValues["lastName"],
        email = formValues["email"]
    };

    await outputDocuments.AddAsync(outputDocument);

    return new OkResult();
}

And that’s it!

The function URL (to be placed in the form’s action attribute) is accessible via the function’s Overview menu. Make sure to prefix it with https://:

Now when I fill in the HTML form and hit the submit button, the field values are automatically saved to a document in my Cosmos DB account:

Restricting access to the Azure Function

As I mentioned earlier, we configured anonymous authentication for the function, so the HTML form can reach it. This also means that anyone else can reach it.

There’s a way to restrict access to the Function App, to only allow applications that you trust.

To configure this, go to the Networking properties of the Function App and open Configure Access Restrictions:

Now add a new rule (or rules) to allow IP addresses you trust, like this:

You can also add Deny rules. The priority determines the order in which the rules are applied.

With the above rule in place, my rules now look like this. It allows 1 IP address and denies all others:

Thanks for reading!

Leave a Reply

Your email address will not be published. Required fields are marked *.

*
*
You may use these <abbr title="HyperText Markup Language">HTML</abbr> tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Theme BCF By aThemeArt - Proudly powered by WordPress .
BACK TO TOP