Going Serverless with Azure Functions: SendGrid
A while back I wrote an app called MyShoppe, which served as a sample application that would enable any business owner with multiple locations to build an app to help their customers get store information and leave feedback. This was more than just a sample application as I built the original application for my brother who has several stores in Texas.
The Old Backend
The app isn’t overly complex. I leveraged Azure Mobile Apps (at the time Mobile Services) as a backend to do online/offline sync with an ASP.NET website so I could write everything in C#. The only thing that the backend was really doing was validating an admin app that I had created when Keith logged in and then it would send an email leveraging SendGrid to Keith whenever someone left feedback.
The New Backend
The app was getting out of date and iOS 10 basically broke the UI, so I had to update it. I decided to simplify the application and get rid of my ASP.NET backend (mosty because I didn’t have the code anywhere) and move to a simplified nodejs backend (even though I hate javascript). The issue of course would be that I would need to handle authentication and sending emails via javascript instead of the nice and pretty C# code that I had in my old ASP.NET backend. No worries though as Azure Functions came to the rescue since I could shove any heavy logic off to the C# world.
Azure Functions 101
If you aren’t aware of what Azure Functions are, let me give you a 101 crash course really quick. Basically think of functions as event based or trigger based methods that get invoked that have inputs and outputs. For instance a trigger could be a HTTP Post and the output could be an email via Send Grid, or perhaps the input is someone uploaded something to blob storage and you want to take it as in input and re-scale it. There are tons of great examples. For my needs I thought… well I could literally send a web request to an Azure Function and write all my parsing and SendGrid logic in C# (Function can actually be written in C#, F#, javascript, and a bunch of other languages).
The App Code
The first step a very simple Azure Mobile App and enabled Easy Tables and Easy APIs which leverage nodejs on the backend.
Inside of my Xamarin apps I made the “Store” table a sync table and the “Feedback” table an ‘online’ only table since I only ever want to push data to the server and don’t need to ever sync to my clients. Essentially here is my code from the app:
if(freedbackTable == null)
feedbackTable = MobileService.GetTable<Feedback> ();
await feedbackTable.InsertAsync(feedback);
With these lines of code the feedback will be sent to my backend and stores in my SQL backend on Azure. I mostly decided to go this route because I want to always have the data backed up instead of just trying to send an email from the app.
The Azure Function Code
Let’s create a function app! So easy, peasy, basically New -> Compute -> Function App. Fill in a bit of information and this Function app contains a suite of functions inside of it. I like to group all functions for an app together.
There are a lot of nice templates to get you started. I went with a simple “HttpTrigger-CSharp” template since I knew that I wanted that to be my input. Essentially I wanted my mobile app backend to call into my Azure function so it is nice and secure and the mobile apps never call the Function directly so it can’t get spammed.
Under the “Integrate” section, you will see an input and output. I deleted the current output and selected a new SendGrid output.
You now need to fill in a bit of information for the output. The most important part here is that the SendGrid API Key doesn’t actually get posted in here, you have to actually add it under your “Function app settings” -> Go to App Service Settings -> Application Settings -> then add a new app setting ->
If you fail to put it in here and if the keys don’t match up it gets all mad. Alright now for the code. This is super easy. I just copied my model into the function, brought in Json.NET and used the SendGrid API. Here is the code:
#r "SendGrid"
#r "Newtonsoft.Json"
using System;
using SendGrid.Helpers.Mail;
using Microsoft.Azure.WebJobs.Host;
using Newtonsoft.Json;
using System.Net;
using System.Collections.Generic;
using System.Linq;
public static Mail Run(HttpRequestMessage req, TraceWriter log)
{
log.Info($"SendGrid Function Triggered");
string jsonContent = req.Content.ReadAsStringAsync().Result;
if(string.IsNullOrWhiteSpace(jsonContent))
{
log.Info($"No data");
return null;
}
var f = JsonConvert.DeserializeObject<Feedback>(jsonContent);
log.Info($"WCSendGrid function processing order: {f.Id}");
Mail message = new Mail()
{
Subject = $"Feedback from {f.Name} {f.VisitDate.ToShortDateString()}!"
};
var service = string.Empty;
switch(f.ServiceType)
{
case 0: service = "Billing and Payments";
break;
case 1:service = "Device Help";
break;
case 2:service = "Upgrade Device";
break;
case 3:service = "New to Store";
break;
case 4:service = "Other";
break;
}
var callback = f.RequiresCall ? "need a callback" : "do NOT need a call";
Content content = new Content
{
Type = "text/plain",
Value = $"Feedback from {f.Name}, for their visit on {f.VisitDate.ToLongDateString()} to store: {f.StoreName}: \n\n" +
$"{f.Text} \n\n" +
$"Their rating was: {f.Rating} \n\n" +
$"Their service type was: {service}\n\n" +
$"Their phone is: {f.PhoneNumber} and they {callback}"
};
message.AddContent(content);
log.Info($"WCSendGrid function processed order: {f.Id}");
return message;
}
public class Feedback
{
[JsonProperty(PropertyName = "id")]
public string Id { get; set; }
[JsonProperty(PropertyName = "text")]
public string Text {get;set;} = string.Empty;
[JsonProperty(PropertyName = "feedbackDate")]
public DateTime FeedbackDate {get;set;} = DateTime.UtcNow;
[JsonProperty(PropertyName = "visitDate")]
public DateTime VisitDate {get;set;} = DateTime.UtcNow;
[JsonProperty(PropertyName = "rating")]
public int Rating {get;set;} = 9;
[JsonProperty(PropertyName = "serviceType")]
public int ServiceType {get;set;} = 4;
[JsonProperty(PropertyName = "name")]
public string Name {get;set;} = string.Empty;
[JsonProperty(PropertyName = "phoneNumber")]
public string PhoneNumber {get;set;} = string.Empty;
[JsonProperty(PropertyName = "requiresCall")]
public bool RequiresCall {get;set;} = false;
[JsonProperty(PropertyName = "storeName")]
public string StoreName { get; set; } = string.Empty;
[JsonIgnore]
public string VisitDateDisplay
{
get { return FeedbackDate.ToString ("g"); }
}
[JsonIgnore]
public string SortBy
{
get { return FeedbackDate.ToString("MMMM yyyy"); }
}
}
It isn’t overly complex once you realize that the return value of the function is the output and the parameters are the input. Besides that it is just standard C#! Beautiful!
Update Backend Logic
For each of the tables there is some javascript essentially that lives behind it like a controller in ASP.NET that has different inserts/update/deletes. So, my plan here was to write as little code as possible and I don’t really know javascript very well so I went to stack overflow and found this bit of code that I could put into the table code behind:
var request = require('http');
table.insert(function (context) {
var item = context.item;
var body = JSON.stringify(item);
var request = new http.ClientRequest({
hostname: "fa.azurewebsites.net",
port: 80,
path: "/api/WCSendGrid?code=YourCode",
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body)
}
});
request.end(body);
return context.execute();
});
This isn’t too bad even though it tooke me like 30 minutes to debug silly hostname issue, but then someone told me about this crazy awesome javascript library called request, which essentially is the new hottness and turned my code into:
var request = require('request');
table.insert(function (context) {
request.post("https://fa.azurewebsites.net/api/WCSendGrid?code=YourCode", {body: context.item, json:true});
return context.execute();
});
And just like that all my logic got placed on to a super cheap Azure Function that I only have to pay per the execution time :) My next steps are to move even more logic to the Azure Function to keep all my costs :)