Azure Functions with F# using .NET 5
Tags: azure, dotnet, fsharp, tutorial
Published: 2021-08-12 11:12:12

Serverless computing promises to make it "easier" to just write some code, sling it at the cloud and voila, you're done. No more worrying about how your code is hosted, configuring application servers or writing hundreds of lines of "boilerplate" to get something running. The reality, however, is a little different, as we found out when trying to write an Azure function that targets the .NET 5 runtime. 

The reason it failed to live up to this promise is that at this point in time the default Azure functions host supports .NET Core 3. So in order to target .NET 5 we have to get our hands dirty and write some boilerplate. Unfortunately, the documentation on how to do this and the project templates for .NET 5 Azure functions don't seem to cover all of the "quirks" that are necessary to make it work.

In this post I'm going to walk through the steps required to get a .NET 5 Azure function running both locally and in Azure and highlight the gotchas that tripped us up when trying to do this. The function itself will be written in F#, but all of the quirks are related to using a .NET 5 runtime and so would equally apply to a C# project. In fact, switching between the two languages is an extremely minimal code change.

The final version of the code shown in this blog post can be found on [GitHub](https://github.com/Choc13/az-function-fsharp-net5).

## "In-process" vs "Out-of-process" hosting

First off, what's a host? In Azure functions the host is the term used to refer to the process that your function will be executed in. It includes a runtime, e.g. dotnet or Python, and all of the external libraries, e.g. DLLs or Python modules, that your code needs to be able to run. 

By default Azure functions uses what it calls an "in-process" hosting model. In practice this means that when you deploy your code as a library, or a script, it runs it in a process with a pre-configured host. 

The upside to this model is that you don't have to worry about infrastructure concerns. You just write your core logic and ship it. This supposedly frees developers from the burden of configuring a host in the entry point to their application, saving them time.

The downside is that if you don't like what's included in the box then your options for altering that are limited to whatever is exposed via the switches and dials they provide. Unfortunately there’s no .NET 5 switch available. This is where the "out-of-process" host comes in.

With out-of-process hosting you get explicit control over how the host is created allowing you to choose a dotnet runtime. When you ship an Azure function using this hosting model you're responsible for configuring the host and making sure it boots correctly. With that background out the way, let's get stuck into the code.

## Create a new project

We're going to need an F# project to work with. It will be edited quite heavily, so the easiest way to create one is to just create a console app by running the following command from the root of your project directory.

```sh
dotnet new console -lang f# -n Function -o src/Function
```

*Note, we've put the project under a `src` directory as is quite typical for dotnet projects, you don't have to follow this convention, but the rest of the steps will assume this project layout.*

## Configure the `.fsproj` file

The first step is to ensure the project file (`.fsproj`) is configured correctly. A minimal example looks like this.

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <AzureFunctionsVersion>v3</AzureFunctionsVersion>
    <_FunctionsSkipCleanOutput>True</_FunctionsSkipCleanOutput>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.4.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Timer" Version="4.1.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.0.4" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Execute.fs" />
    <Compile Include="Program.fs" />
  </ItemGroup>
  <ItemGroup>
    <None Include="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Include="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>
```

You can just go ahead and replace the contents of the generated `Function.fsproj` file with the above.

There are a few things to point out that are required to make this work which weren't documented or included in the official code templates.

1. The `_FunctionsSkipCleanOutput` must be set to `True`.
1. The directives for the `host.json` and `local.settings.json` must use `<None Include=>`. The functions templates generate these with `<None Update=>` which doesn't work.
1. The templates also don't include the correct package references. At a minimum we need the ones listed above, at their latest versions, in order to run on .NET 5.

     *Note this example is using `Microsoft.Azure.Functions.Worker.Extensions.Timer` because this basic example just uses a simple timer trigger, you might need a different extension package if you're using a different trigger, e.g. Blob triggers require `Microsoft.Azure.Functions.Worker.Extensions.Storage`.*

## Configure `host.json`

We need to add a `host.json` file to the project with the following contents.

```json
{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "samplingExcludedTypes": "Request"
      }
    }
  }
}
```

There aren't any quirks here, so just add that and move on.

## Add a `local.settings.json`

We also need a `local.settings.json` file in the project which should look like this.

```json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
  }
}
```

The crucial bit here is that we've set `FUNCTIONS_WORKER_RUNTIME` to `"dotnet-isolated"`. This is how we specify that we want to use an out-of-process host.

## Configure the host in `Program.fs`

Because we've opted to go out-of-process we need to write the host bootstrapping code in `Program.fs`.

```fsharp
open Microsoft.Extensions.Hosting

HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .Build()
    .Run()
```

As you can see, the boilerplate is actually very minimal for this simple function. Also, the nice thing about using the `dotnet-isolated` runtime and explicitly defining how the host is bootstrapped is that if the application grows and requires interaction with more azure services, such as blob storage or message queues, then we get complete control over how those services are configured. We're also able to specify exactly which NuGet packages, at which versions, are included in the runtime too. In my experience this leads to much fewer surprises and less time spent opaquely debugging in Azure, as compared to using an in-process host in which issues such as assembly conflicts occur at runtime. Give me that sweet, sweet, boilerplate every time.

## Add your functions

We can now go ahead and write our functions. Our project is configured to expect a file called `Execute.fs`, which is where we're going to place them. There's nothing special about that file name, so feel free to call it whatever you want, or arrange your functions in multiple files if that suits your needs better.

Here's what our simple example timer function looks like in F#.

```fsharp
namespace My.Function

open Microsoft.Azure.Functions.Worker
open Microsoft.Extensions.Logging

type Execute(logger: ILogger<Execute>) =
    [<Function("Execute")>]
    member _.Run([<TimerTrigger("0 */5 * * * *")>] timer: TimerInfo) =
        logger.LogInformation($"Hello at {System.DateTime.UtcNow} from an Azure function using F# on .NET 5.")
```

Somewhat annoyingly, we have to add the `TimerTrigger` attribute to a method parameter, rather than being able to specify it at the method level. So even if we don't want to use the `TimerInfo`, we need to accept it as an argument.

Also, ironically, we have to create a class to write a "function" 😜 

## Building the code

This one should be fairly straight forward, but there's a gotcha here too. In order to build an Azure function running on .NET 5 you'll need the .NET Core 3.1 SDK installed. There's more information in this [GitHub](https://github.com/Azure/azure-functions-dotnet-worker/issues/480) issue. Once you've got that installed you can just run `dotnet build` like usual.  

## Run it locally

At this point we can run the function locally using the [Azure Function Tools](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local) and the [Azurite](https://docs.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=npm) storage emulator.

With these two tools installed you can run the following commands (in separate terminals or as background processes) to start the function.

```sh
azurite --location ~/.azurite --debug ~/.azurite/debug.log
func start
```

The output from `func start` should look something like this.

![functions-output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ysv9cfopzdyy3h740bv.png)

## Deploy to Azure

In order to deploy to Azure we need to create some Azure resources. At a minimum we need a resource group containing a storage account, app service plan on the consumption billing tier and a function app. We can configure all of this from an ARM template. Whilst straight forward if you know how to use ARM templates, it's quite verbose, so instead of inlining it here, you can see it on [GitHub](https://github.com/Choc13/az-function-fsharp-net5/blob/master/azuredeploy.json) instead.

Assuming you've got an Azure subscription and the [Az CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) installed then we can deploy it with these commands.

```sh
# Create a resource group, only needs to be done the first time
az group create -n <resource-group-name> -l <location>

# Build the code, this will publish all the necessary files to the location specified by the -o argument
dotnet build src/Function -c Release -o .publish/func

# The template outputs the function app's name so we can use it in the next step to deploy the code
FUNCTION_NAME=$(az deployment group create \
    --no-prompt \
    --output tsv \
    --query properties.outputs.functionName.value \
    --resource-group <resource-group-name> \
    --template-file ./azuredeploy.json)

cd .publish/func

# Zip up the build output for deployment to the app
zip -r ./funtionapp.zip .

az functionapp deployment source config-zip \
    --resource-group <resource-group-name> \
    --name ${FUNCTION_NAME} \
    --src ./functionapp.zip
```

One quirk here is that you might have been expecting to use the `dotnet publish` command in order to create the deployable artefact, as is typical when developing an ASP.NET app. That's not the case here, for Azure functions the publishing happens automatically when running `dotnet build`. 

## Bonus: Build and Deploy from GitHub

Now that we've automated the build and deployment using CLI tools, it's actually very easy to turn this into a GitHub action. Especially if we create a script called `deploy.sh` that takes care of all of the Azure related steps from the previous section.

```yml
name: Build & Deploy
on:
  push:
    branches:
    - master
  pull_request:
env:
  FUNCTION_PACKAGE_PATH: .publish/function
  RESOURCE_GROUP: az-function-fsharp-net5
jobs:
  build-and-deploy:
    runs-on: ubuntu-18.04
    steps:
    - name: Checkout code
      uses: actions/checkout@master
      with:
        fetch-depth: 0
    - name: Setup dotnet SDK 3.1 (https://github.com/Azure/azure-functions-dotnet-worker/issues/480)
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: "3.1.409"
    - name: Setup dotnet SDK
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: "5.0.300"
    - name: Publish Function
      run: dotnet build src/Function -c Release -o ${{ env.FUNCTION_PACKAGE_PATH }}
    - name: Login to Azure
      uses: azure/login@v1
      with:
        creds: ${{ secrets.AZURE_RBAC_CREDENTIALS }}
    - name: Deploy to Azure
      uses: azure/CLI@v1
      with:
        inlineScript: ./deploy.sh ${{ env.FUNCTION_PACKAGE_PATH }} -g ${{ env.RESOURCE_GROUP }}
```

Note that we have to install both the .NET Core 3.1 and .NET 5 SDKs in order to build the code.

In order for this to work you'll need to set a GitHub secret called `AZURE_RBAC_CREDENTIALS` in your repo. More details about how to generate and set these credentials can be found on the [Azure Login Action's page](https://github.com/marketplace/actions/azure-login#configure-deployment-credentials).

## See the full example on GitHub

If you want to see the final example all in one place then you can check it out on GitHub. Feel free to clone it or fork it and use it as a starting template.

{% github Choc13/az-function-fsharp-net5 %}

## The state of Azure severless in 2021

Given the variety of triggers available and the consumption based billing model Azure functions are well suited for running reactive asynchronous background tasks. This is an important piece in the architecture of any reasonably sized distributed system hosted in the cloud. It also provides a complimentary role to that of the backend API, such as a REST API, which expects to make quick decisions and not spend its time chugging away at long running async computations.

It's a shame then that Azure functions seem to have conflated this feature set with the one of reducing-all-the-boilerplate. It's understandable that there is a use case that exists in which many people will just want to write some code and get it running somewhere with minimal fuss. Unfortunately, I think that's a completely different set of people to the ones who want to use Azure functions as part of the architecture of a larger distributed system.

As someone in the latter camp I care much more about being able to have explicit control over the environment in which my code runs than I do about eliminating a dozen lines of host configuration boilerplate. Spending time eliminating this boilerplate is a false economy because I probably spend less than 0.001% of my time when building such a system on those dozen lines of config code. The problem with trying to eliminate all the boilerplate is that if you don't nail the abstractions they end up creating more friction than the original boilerplate did, by forcing developers to figure out how to work around them.

## Conclusion

Creating an Azure function targeting the latest .NET runtime is quite fiddly and insufficiently documented at present. Fortunately, with a bit of tinkering it is possible to make it work. Choosing between F# and C# is also just a matter of which language suits your project better as neither requires any extra Azure functions magic than the other.

In the future it would be great the see Microsoft focus more on building a solid pay-per-use background processing solution without all the magic, basically good old WebJobs on a consumption plan. The "in-process" use case should then be simple to build on top of this foundation for those that don't need such control and don't want to have to figure out how to configure a host. Better to try and walk before you can run.