Using GCP Cloud Functions with F#

Das Logo der TIMETOACT GROUP Österreich

by TIMETOACT GROUP Österreich Marketing

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:

type Startup() =
    inherit FunctionsStartup()

    override this.ConfigureServices(context: WebHostBuilderContext, services: IServiceCollection) =
        match context.Configuration.ApiPublicKey, context.Configuration.ApiPrivateKey with
        | Some apiKey, Some apiSecret ->

            services.AddSingleton<IEmailClient>(fun _ ->
                EmailService.EmailClient(apiKey, apiSecret) :> IEmailClient)
            |> ignore
        | _ -> failwith "The PUBLIC_KEY and PRIVATE_KEY environment variables are required."

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:

module ConfigExtensions =
    open System
    open Microsoft.Extensions.Configuration

    let nonEmptyStringOption value =
        value
        |> Option.ofObj
        |> Option.filter (String.IsNullOrWhiteSpace >> not)

    type IConfiguration with

        member this.ApiPublicKey =
            this.GetValue<string> "API_PUBLIC_KEY"
            |> nonEmptyStringOption

        member this.ApiPrivateKey =
            this.GetValue<string> "API_PRIVATE_KEY"
            |> nonEmptyStringOption

Next up we want to implement our cloud function:

[<FunctionsStartup(typeof<Startup>)>]
type Function(logger: ILogger<Function>, emailClient: IEmailClient) =
    interface ICloudEventFunction<MessagePublishedData> with

      member this.HandleAsync(cloudEvent, data, cancellationToken) = task {

        let myMessage = JsonEncoding.fromJson<Message>(data.Message.TextData)
      logger.LogInformation("Sending email to {person}", myMessage.To)
        return! emailClient.Send message.To "Success!"
        }

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.

type TestStartup() =
    inherit Startup()

    override this.ConfigureAppConfiguration(context: WebHostBuilderContext, configuration: IConfigurationBuilder) =
        configuration.AddInMemoryCollection
            ([ KeyValuePair<string, string>("API_PUBLIC_KEY", "123")
               KeyValuePair<string, string>("API_PRIVATE_KEY", "456") ])
        |> ignore

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:

[<FunctionsStartup(typeof<TestStartup>)>]
type FunctionTests() =
    inherit FunctionTestBase<Function>()

    let messageJson = """{
       "To":"jonathan@theinternet.com"
    }"""

    member this.ExecuteCloudEventRequestAsync cloudEvent =
        base.ExecuteCloudEventRequestAsync cloudEvent

    [<Fact>]
    member this.functionTest() =
        task {


            let message = PubsubMessage()
            message.TextData <- messageJson

            let data = MessagePublishedData()
            data.Message <- message

            let cloudEvent =
                CloudEvent
                    (MessagePublishedData.MessagePublishedCloudEventType,
                     Uri("//help", UriKind.RelativeOrAbsolute),
                     null,
                     Nullable())

            CloudEventConverters.PopulateCloudEvent(cloudEvent, data)

            do! this.ExecuteCloudEventRequestAsync(cloudEvent)

                        //Assert whatever you feel is best here
            Assert.Equal(1, 1)
        }

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!

1/8/21
Blog

Function Composition in F# with Unfriendly Functions

Explore how to handle unfriendly library functions in F# by using wrappers, higher-order functions, or inline solutions to keep pipelines clean and functional.

Blog 11/30/22

Introduction to Partial Function Application in F#

Partial Function Application is one of the core functional programming concepts that everyone should understand as it is widely used in most F# codebases.In this post I will introduce you to the grace and power of partial application. We will start with tupled arguments that most devs will recognise and then move onto curried arguments that allow us to use partial application.

Blog

Functional Validation in F# Using Applicatives

Learn how to implement functional validation in F# using applicatives. Handle multiple errors elegantly with Railway-Oriented Programming and concise functional patterns.

Blog

Solving Transport Tycoon 2 Episode 2.1 With F#

Solve Transport Tycoon 2.1 in F#: explore recursion, rose trees, and functional techniques to compute the shortest railway routes between stations.

Blog 3/10/21

Introduction to Web Programming in F# with Giraffe – Part 1

In this series we are investigating web programming with Giraffe and the Giraffe View Engine plus a few other useful F# libraries.

Blog 3/11/21

Introduction to Web Programming in F# with Giraffe – Part 2

In this series we are investigating web programming with Giraffe and the Giraffe View Engine plus a few other useful F# libraries.

Blog 3/12/21

Introduction to Web Programming in F# with Giraffe – Part 3

In this series we are investigating web programming with Giraffe and the Giraffe View Engine plus a few other useful F# libraries.

Blog 12/3/21

Using Discriminated Union Labelled Fields

A few weeks ago, I re-discovered labelled fields in discriminated unions. Despite the fact that they look like tuples, they are not.

young colleagues working together it jobs timetoact group
Jobs

(Senior) Consultant SAP Analytics Cloud (f/m/d)

Walldorf Consulting | Walldorf | Full-time & Permanent employment | Available now

Blog

Celebrating Homai - Using AI for Good

Our colleague Aigiz Kunafin has achieved an outstanding milestone - importance of his side-project Homai was acknowledged by the “AI for Good” Initiative of United Nations.

Blog 3/17/22

Using NLP libraries for post-processing

Learn how to analyse sticky notes in miro from event stormings and how this analysis can be carried out with the help of the spaCy library.

Blog 8/7/20

Understanding F# Type Aliases

In this post, we discuss the difference between F# types and aliases that from a glance may appear to be the same thing.

Blog

Using Historical Data to Simulate Truck Journey

Discover how historical truck data and Python simulations can predict journey times and CO₂ emissions, helping logistics become smarter and greener.

Schulung 6/2/23

Practice-oriented training for Google Workspace and GCP

Improve your skills in Google Workspace and GCP with our hands-on training courses. Optimize your workflows and increase your performance.

Blog 5/18/22

Introduction to Functional Programming in F#

Dive into functional programming with F# in our introductory series. Learn how to solve real business problems using F#'s functional programming features. This first part covers setting up your environment, basic F# syntax, and implementing a simple use case. Perfect for developers looking to enhance their skills in functional programming.

Blog 6/24/21

Using a Skill/Will matrix for personal career development

Discover how a Skill/Will Matrix helps employees identify strengths and areas for growth, boosting personal and professional development.

Success Story

TIMETOACT GROUP - Digital Transformation with SAP Cloud ERP

Learn more about the successful implementation of SAP S/4HANA Cloud Public Edition – powered by a strong partnership between TIMETOACT and Walldorf Consulting.

Guide

Future-Proof Your Business with SAP Cloud ERP

Discover how SAP Cloud ERP transforms business operations with enhanced agility, reduced costs, and real-time decision-making. Download our free guide and future-proof your organization.

Blog 7/21/20

Understanding F# applicatives and custom operators

In this post, Jonathan Channon, a newcomer to F#, discusses how he learnt about a slightly more advanced functional concept — Applicatives.

Whitepaper

HBR - Driving Growth with Cloud ERP for Services Companies

If your business depends on billable hours, then this paper’s for you. Read in this Harvard Business Review Analytic Services paper how cloud ERP can drive growth for professional services firms.

Bleiben Sie mit dem TIMETOACT GROUP Newsletter auf dem Laufenden!