Migrating function app scripts to a class library

In the previous post we created a trio of function apps to send Slack notifications for OMS alerts based on CPU, Memory, and Disk. These were created using function app scripts, and the code was stored and tested directly in the Azure portal.

In this post we'll walk through the steps required to replace those scripts with a class library that exposes three functions that can be hosted by the Azure function app runtime. It will do exactly the same thing, but crucially will allow us to develop & test locally.

If you want to skp the interim steps and get straight to the finished code it is linked below. The code also contains the sample payloads, as well as a sample Pester test that validates the function completes without an error (returns a HTTP 200).

Migrating the function scripts

Create a new function app project (this step needs Visual Studio 2017 15.3). If you don't see the option to create function apps you might not have installed the Azure development workload. The official documentation walks through what is needed to build a function app with Visual Studio. (repo)

Now copy the script of the three functions into three separate files in the project folder. These are .csx (C# script) files in the portal, but we'll need to rename them to be .cs (C#) files. Right now the solution won't build, and we'll need to make a few changes to turn the C# scripts into valid C# files. (repo)

To get the solution to build:

  • Remove the first line of each script (#r "Newtonsoft.Json") - the Newtonsoft.Json package is already referenced by the Functions SDK, and references are managed at the project level rather than the file level
  • Wrap the Run method and supporting classes in a class (one class per file, use the name of the file so that the file CPUToSlack.cs would contain public class CPUToSlack)
  • Add usings for System.Threading.Tasks, System.Collections.Generic, and System.Linq, System.Net.Http, and Microsoft.Azure.WebJobs.Host

The project will now build. (repo)

If you run the project you'll see the function runtime start but an error message indicates that your app doesn't contain any functions.

No Function Found

To have our class library 'light up' as a function we need to tell the functions runtime which methods should be treated as 'functions'. We do this by specifying a FunctionName attribute on each method we wish to host as a function. We also need to provide some additional information about the input parameter (which for us is a webhook) so the function runtime knows what that function expects. Finally, we'll also need to add a using for Microsoft.Azure.WebJobs. (repo)

public class MemoryToSlack
{
[FunctionName("MemoryToSlack")]
public static async Task<object> Run([HttpTrigger(WebHookType = "genericJson")]HttpRequestMessage req, TraceWriter log)
{

Running the function app again will show us that the runtime has discovered our functions and is now listening on a URL.

Function Found

To test the functions I found it easiest to use the json payloads we created earlier and post them to the function using Powershell. The below code is a Pester test that will post the relevant payload to each function, and return success only if the function returns a HTTP 200 (OK) response code.

OMSToSlack.tests.ps1

$port = 7071
$uriBase = "http://localhost:$port/api"
$cpu = Get-Content cpu-payload.json
$memory = Get-Content memory-payload.json
$drive = Get-Content drive-payload.json

Describe "OMS to Slack Function" {
It "Should return a 200 for a CPU payload" {
(Invoke-WebRequest -Uri "$uriBase/CPUToSlack" -Body $cpu -Method Post -ContentType "text/json").StatusCode | Should Be 200
}

It "Should return a 200 for a Memory payload" {
(Invoke-WebRequest -Uri "$uriBase/MemoryToSlack" -Body $memory -Method Post -ContentType "text/json").StatusCode | Should Be 200
}

It "Should return a 200 for a Drive payload" {
(Invoke-WebRequest -Uri "$uriBase/DriveToSlack " -Body $drive -Method Post -ContentType "text/json").StatusCode | Should Be 200
}
}

We can run the test by running the Invoke-Pester command in the folder that contains the file. A successful execution tells us that our functions are performing as we expect (or at least that they aren't throwing any exception!). As we make changes to the function code this test will allow us to verify that our functions still complete when we provide our known-good payloads. (repo)

Pester Test Success

The class library is now ready to be deployed and replace the script functions. To deploy and overwrite your functions you can download a publish profile from the Azure portal:

Download Publish Profile

And then when you publish the project in Visual Studio select 'Import profile', and then Publish. If you browse to the Azure portal you'll notice that you can no longer see your function code (the .csx files), and instead you see the function.json file that points to your class library and entry point:

Function JSON

You can still test the function from the portal and watch the logs. You can also use the Pester test to run payloads against your live functions.

Next steps

As a final step I've made a few changes to the repo so that the current state no longer contains the duplicate class definitions in each of our functions (e.g. the OMSPayload class, the SlackMessage class).

We'll be making several changes to the functions next time to extract common functionality, promote some values to config, and then finally start extending the functions to provide the features I've talked about in previous posts.