Previously we looked at Building Azure Functions with F# Script, which is still the only supported way of creating an Azure Function with F#, and a decent option for simple functions. So, if you need to play it safe (and stable) then that’s your best bet. However, in this post we are going to take a trip to the edge. We’ll be using .NET Core, Azure Functions Core Tools v2, and F#. However, instead of F# Script we’ll be creating a precompiled app!

Being on the bleeding edge has its drawbacks however. Chances are we’re going to see some bugs. The Core Tools are in beta after all, so that should be expected. I will explain how I work around the current issues, but chances are things will change between now and the time you read this. So if you don’t encounter an issue we’re solving here, that’s fantastic. It means Azure Functions with F# is moving in the right direction!

So without further ado, let’s get started.

Setup Your Environment Link to heading

If you’re not already setup for F# Azure Functions development, you can follow the same instructions from my previous post, with one exception. You will need to install v2 instead of v1 of the Azure Functions Core Tools. I would recommend you install it with npm instead of Chocolatey. I had some issues using the Chocolatey version and never got to the bottom of it.

To install v2 of the Core Tools use this command:

npm i -g azure-functions-core-tools@core --unsafe-perm true

Also, did I say “one exception”? I meant two. You’ll also need to install the .NET Core SDK.

Okay… three. Sheesh. I’m also going to use the NuGet Package Manager extension in Visual Studio Code, so you might as well install that now as well.

Note
You could use Paket to manage the dependencies instead of NuGet. It’s far more popular within the F# community. But I want to limit the number of new technologies for newcomers to F# in these posts. I’ll get to Paket one day.

Create The Function App Link to heading

Now that you have what you need, let’s create our app. There is no F# template available yet, so we’re going to cheat a little and use the C# template and just change a few things. Here we go…

  • Create a new directory (call it what you want)
  • Open VS Code
  • Invoke Azure Functions: Create New Project in the Command Pallette
  • Follow the prompts:
    • Select the folder that will contain your function app: Press [Enter] to pick the current folder
    • Select a language for your function project: Pick C#

You now have an empty Function App, initialized as a git repository. There’s a .gitignore file tailor-made for Azure Functions development.

The first thing we should do is rename the .csproj to .fsproj. While you’re in there, notice that we’re using the new project file format, as well as netstandard2.0.

Besides the Git repo and project file, there are actually quite a few goodies nestled in the .vscode folder.

VS Code Goodies Link to heading

The C# template is configured for making Azure Functions development in VS Code an awesome experience. However, not all of it is going to help us with F#. So let’s crack open each of these configuration files and see what we need to change. Expand the .vscode folder in VS Code, and let’s dig in.

extensions.json Link to heading

This file tells VS Code what extensions to recommend for the project. Obviously, we won’t be needing the ms-vscode.csharp extension, so you can remove it.

launch.json Link to heading

Here we find a configuration that allows us to attach a debugger to our function. How awesome is that! Also, you should rename C# to F# in the name field. Sure it’s a minor detail, but those who know me know I can’t live with these kind of inconsistencies! I feel better now. Do you?

settings.json Link to heading

This is where you customize VS Code’s workspace settings. In this case, there’s some configuration for the Azure Functions extension. We should change azureFunctions.projectLanguage to F#. However, it might be useful to leave this alone so we can generate new Functions based on their C# templates and simply port them to F#. I’ll leave this decision up to you.

tasks.json Link to heading

Last, but certainly not least, are the three tasks that allow you to clean, build, and run your Function App from the VS Code Command Palette. Or, if you prefer, using the following keyboard shortcuts:

If You Build It, They Will… Oh Link to heading

If you attempt to build the app now, you might see the following warning in the terminal. If not, feel free to skip this section.

warning NU1701: Package ‘Microsoft.AspNet.WebApi.Client 5.2.2’ was restored using ‘.NETFramework,Version=v4.6.1’ instead of the project target framework ‘.NETStandard,Version=v2.0’. This package may not be fully compatible with your project.

To fix this, we’ll need to update our version of Microsoft.NET.Sdk.Functions. Open the Command Palette and invoke NuGet Package Manager: Add Package and search for Microsoft.NET.Sdk.Functions. Select the latest version and continue. When I wrote this, the latest version was 1.0.13.

Once that completes you should be able to build without warnings.

Create a Function Link to heading

As mentioned above, there are no F# templates yet. So the Azure Functions extension can’t help us to create a new F# function. If you chose not to change azureFunctions.projectLanguage to F# in the settings.json, then you could generate one in C# and convert it. But that’s not what we’re going to do. We’ll learn more by creating one from scratch.

We will use the new Attributed Model to define our function. This means we don’t have to manage the function.json file anymore. It’s generated at compile time based on the attributes! Full disclosure, the attributes are kinda ugly, but we’ll explore a strategy to deal with that later.

Like last time, we’re going to create an HttpTrigger function called HelloYou. Start by create a new file at the root of your project called HelloYou.fs. You’ll have to add a reference to the new file in your .fsproj. The Ionide plugin provides a quick way to do this via the F#: Add Current File To Project command. However, when I wrote this it wasn’t working. So make sure to check the project file has this added to it:

  <ItemGroup>
    <Compile Include="HelloYou.fs" />
  </ItemGroup>

Add the following code to HelloYou.fs. We’re going to keep things simple for now and just echo back “Hello”. It’s a start.

namespace MyFunctions

open Microsoft.Azure.WebJobs
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Mvc

module HelloYou =
    [<FunctionName("HelloYou")>]
    let run
        ([<HttpTrigger(Extensions.Http.AuthorizationLevel.Anonymous, "get", Route = "hello")>]
        req: HttpRequest) =
            ContentResult(Content = "Hello", ContentType = "text/html")

Check out those ugly attributes Link to heading

Personally, I think the attributes are kinda ugly. But their inner beauty is not have to maintain a function.json. Let’s take a quick peek at the two we’re using here.

FunctionName Link to heading

The FunctionName attribute marks this as a function entry point. This name must be unique within your Function App. It also let’s you name the function in code whatever you want, e.g. run.

HttpTrigger Link to heading

All functions need to be triggered somehow. In our case, we’re using the HttpTrigger attribute to indicate this function is triggered by an HTTP call. It takes several parameters:

  • authLevel: Sets the authorization level for the function, i.e. what keys need to be provided to invoke the function.
  • methods: Indicates which HTTP verbs the function is triggered by. In our case we’re just using GET
  • route: The route to our function

If You Run It, They Will… Oh Link to heading

I liked that heading so much I thought I’d reuse it for our second gotcha!

If you run function, you might see the following error:

System.Private.CoreLib: Could not load file or assembly ‘FSharp.Core, Version=4.4.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a’. Could not find or load a specific file.

If you don’t see this, fantastic! Move on to the next section. But if you do… This is caused by an assembly version mismatch in the Azure Functions Core Tools runtime. In this case, it’s the FSharp.Core assembly. Like you, I am using the latest F# version on my machine, so our app is attempting to load FSharp.Core, Version=4.4.3.0. But the Azure Functions runtime has only loaded v4.2.3. To fix this, we will install the FSharp.Core NuGet package for the version used by the Azure Functions runtime.

Stop the Function Host if it’s still running (press Ctrl+C in the terminal). Then invoke the NuGet Package Manager: Add Package command and find FSharp.Core. Install version 4.2.3. Once complete you should see a new PackageReference in the .fsproj for FSharp.Core.

If You Run It, It Will… Run! Link to heading

Okay, NOW you can start the function host and everything should be just hunky dory. If you Ctrl+Click the HelloYou link http://localhost:7071/api/hello in the terminal window your browser should open up a page that says “Hello”.

Amazing! Look at us fancy programmers with our “Hello” string. But we can do better…

Let’s Deal With Those Attributes Link to heading

I think I read somewhere that it’s a good practice to keep the attributed function declarations separate from your actual code. But in case I’m imagining that, then you can say you read it here first.

Let’s create a Functions.fs file at the root of our project and put our attributed function there. Don’t forget to update the .fsproj.

Functions.fs Link to heading

namespace MyFunctions

open Microsoft.Azure.WebJobs
open Microsoft.AspNetCore.Http

module Functions =

    [<FunctionName("HelloYou")>]
    let helloYou
        ([<HttpTrigger(Extensions.Http.AuthorizationLevel.Anonymous, "get", Route = "hello")>]
        req: HttpRequest) =
            HelloYou.run req

fsproj Link to heading

The ordering here is important!

  <ItemGroup>
    <Compile Include="HelloYou.fs"/>
    <Compile Include="Functions.fs"/>
  </ItemGroup>

HelloYou.fs Link to heading

namespace MyFunctions

open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Mvc

module HelloYou =
    let run (req: HttpRequest) =
        ContentResult(Content = "Hello", ContentType = "text/html")

Now that the Azure Functions stuff is separated, we are free to write more canonical F# in our HelloYou.fs. Make sure you run the function again to make sure it still works.

This Isn’t Even My Final Form Link to heading

Now that we’ve got things running, and we’ve shunted our attributes to a separate file, let’s fill out the rest of our function!

Note
Instead of copy/pasting the code below, try writing it yourself. Use the auto-open feature of Ionide to automatically add the necessary open statements for your code. Simply write part of the identifier, e.g. TraceWr and when the intellisense suggests: Auto-Open Intellisense hit [Enter] to complete the statement and add the open statement at the top of the file.

Changes to Functions.fs Link to heading

We’re going to add some logging to our function. So we’ll need to add a log: TraceWriter parameter to our function declaration. We’re also going to use HTTP POST instead of GET, so we’ll need to update the list of supported methods.

Our Functions.fs now looks like this:

namespace MyFunctions

open Microsoft.Azure.WebJobs
open Microsoft.AspNetCore.Http
open Microsoft.Azure.WebJobs.Host

module Functions =

    [<FunctionName("HelloYou")>]
    let helloYou
        ([<HttpTrigger(Extensions.Http.AuthorizationLevel.Anonymous, "post", Route = "hello")>]
        req: HttpRequest,
        log: TraceWriter) =
            HelloYou.run req log

Changes to HelloYou.fs Link to heading

The HelloYou.run function itself is going to have a number of changes.

  • We’re adding the ability to take some input JSON, which needs to be deserialized. For that, we define a new record type called InputModel
  • We’ll then read the req.Body using a StreamReader
  • Deserialize the input with JsonConvert.DeserializeObject
  • Do some quick validation of the input to ensure both FirstName and LastName are provided
  • If our validation fails we return a BadRequestObjectResult
  • Otherwise we return an OkObjectResult with our response
  • We’ve also added some logging, and wrapped the whole thing in an async workflow
Note
Notice that we have to cast these responses to the same result type using :> IActionResult in order to appease the F# compiler.

Our HelloYou.fs now looks like this:

namespace MyFunctions

open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Mvc
open Microsoft.Azure.WebJobs.Host
open System.IO
open Newtonsoft.Json
open System

module HelloYou =
    type InputModel = {
        FirstName: string
        LastName: string
    }

    exception InvalidInputException of string

    let run (req: HttpRequest) (log: TraceWriter) =
        log.Info "[Enter] HelloYou.run"
        async {
            use stream = new StreamReader(req.Body)
            let! body = stream.ReadToEndAsync() |> Async.AwaitTask
            let input = JsonConvert.DeserializeObject<InputModel>(body)
            if (String.IsNullOrWhiteSpace input.FirstName) || (String.IsNullOrWhiteSpace input.LastName) then
                log.Info "Received by input"
                return BadRequestObjectResult "Please pass a JSON object with a FirstName and a LastName." :> IActionResult
            else
                log.Info "Received good input"
                return OkObjectResult (sprintf "Hello, %s %s" input.FirstName input.LastName) :> IActionResult
        }
        |> Async.RunSynchronously

You should be able to run this locally now and test it with Postman. Here’s what a good request looks like:

Postman Request

Notice, there is no error handling. So if you pass some invalid input you’ll get a 500 Internal Server Error back.

A Final Note About the function.json Link to heading

Without the function attributes we would’ve needed to create and maintain a function.json file. Fortunately, this gets generated for us at compile time and can be found in bin\Debug\netstandard2.0\HelloYou\function.json. It looks like this:

{
  "generatedBy": "Microsoft.NET.Sdk.Functions-1.0.13",
  "configurationSource": "attributes",
  "bindings": [
    {
      "type": "httpTrigger",
      "route": "hello",
      "methods": [
        "post"
      ],
      "authLevel": "anonymous",
      "name": "req"
    }
  ],
  "disabled": false,
  "scriptFile": "../bin/building-azure-functions-with-precompiled-fsharp.dll",
  "entryPoint": "MyFunctions.Functions.helloYou"
}

Notice that it calls out the fact it’s a generated file with the generatedBy property. And check out those bindings. You can see in the req binding it lists the only method supported is post, and the authLevel we specified is set to anonymous. It’s got our route specified there as well.

Two other properties, essential to a precompiled app, are listed at the end:

  • scriptFile: Point to the compiled assembly
  • entryPoint: Indicates the [namespace].[module].[function] of the function, in this case MyFunctions.Functions.helloYou

What’s Next Link to heading

We covered a ton of ground on this one! Working with Pre-Compiled apps gives us a ton of flexibility. Also, we didn’t need that weird Editor Prelude thing. But we did lose the slickness of auto-reloading on changes that is supported by F# (and C#) Script. The good news is, that’s been implemented already and will likely be included in the next beta release of the core tools!

Why not try deploying this to your Azure account. Once you do, notice that none of the code is editable in the Azure Portal. That’s because it’s a precompiled app. The only thing you’ll see there is the generated function.json.

Oh, and try attaching the debugger to your function! Just run the function, set a breakpoint, and press F5. Super easy!

That’s it for now. And thanks for reading!