Using GCP Cloud Functions with F#
I've recently been writing a new feature in one of our projects and I thought I'd take advantage of the new .NET support in GCP Cloud Functions. As the project I work on is fully F# based my cloud function was going to be written in F#. There is a great in depth introduction to .NET support in GCP by Jon Skeet that can be found here.
I thought it might be useful to write a short blog post on how you might write a simple pub/sub cloud function in F#. I will assume you have read the earlier blog post and instead of creating a C# project you have created a F# project.
Our app is going to subscribe to a topic we have already deployed, deserialize the message and send an email to someone. We will need to use dependency injection to wire up our email client and to autmotically allow us to write some logs and use the framework's configuration.
If you are used to ASP.NET Core none of this will be a shock as it's very similar. First thing we will want to do is create our own Startup type:
So here we are creating our type that inheits from the GCP FunctionsStartup type and then overrides the ConfigureServices function. We then check our configuration object has some keys set and if so register our IEmailClient
object otherwise we throw an exception.
The nice thing with F# is that you can easily extend defined interfaces so you may spotted the match
statement using properties called ApiPublicKey/ApiPrivateKey. This comes from extending Microsoft's IConfiguration
like so:
Next up we want to implement our cloud function:
Excuse the brevity of the function but I just wanted to illustrate the main elements in play here. You'll see that we have to apply an attribute to the function so that it knows which Startup type to use so it knows how to register dependencies for example. You'll also see our function has a ILogger
injected, this comes from the library itself and the wiring up it does under the hood and then it takes in IEmailClient
that we registered in our Startup class.
Our type then has to implement an interface from the GCP nugget packages we installed and call a HandleAsync
function. To get an object from the pub/sub message you'll see we use the data
object passed into the function to deserialize it into our defined type. We can then use the properties on our type to do things with such as log who we're going to send a message to and also actually use it to send an email to!
I'll briefly touch on testing functions locally now. Helpfully, GCP provides a testing library that wraps .NET's TestServer and provides many things to allow you to test your function. Create a new project in your solution that references your function project and also has Google.Cloud.Functions.Testing, Microsoft.NET.Test.Sdk, xunit, xunit.runner.visualstudio
nuget packages installed.
As our function needs configuration values set we can create a new TestStartup type that inherits from our Startup type and passes in configuration just for testing purposes. We can then write some tests:
As you'll see even in tests we need to add an attribute to tell our test objects which Startup to use. We then inherit of a GCP type that handles sending messages to a function and pass in our function type as a generic argument. We then have a string literal of what our message looks like that comes from our pub/sub topic in GCP. We then have to define our own function that calls the base ExecuteCloudEventRequestAsync
function. This is an F# idiosyncrasy in that we cannot call base
functions directly due to scope issues and therefore have to be called via type members. We can then write our xUnit test by applying a Fact
attribute. To mimic a pub/sub subscription calling our function we have to create some objects and then call the GCP test framework's ExecuteCloudEventRequestAsync
which will call our function.
Once we have called our function we can then assert something. Obviously in a pub/sub scenario we don't get any response objects back so we can't assert anything on a response. In this scenario we could have a in-mem email client that implements the same interface we register in our function and when called it adds the message to a list , we could then assert that our in-mem email client has a message list length greater than 0 but you do whatever you feel is best. You'll notice that the objects that we have to call in our test are very C#-y as in we have to set properties using the <-
operator. You'll also see we have to pass in null
and Nullable()
arguments to CloudEvent
constructor. This is because in C# these are optional arguments and in this scenario we have to be explicit. For future F# users of GCP test framework I have already submitted a PR that should tidy this up a bit for future users!
Anyhow, I hope this brief introduction to GCP Cloud Functions for F# users has been of some help!