United Kingdom: +44 (0)208 088 8978

Azure Functions with F# – From Scratch

We're hiring Software Developers

Click here to find out more

What are we going to do?

  • Set up our prerequisites and dev environment
  • Create an Azure Functions F# project from CLI
  • Use Visual Studio Code to edit and run/debug the project locally
  • Deploy the functions to Azure from Visual Studio Code

We will be dealing with compiled Azure Functions. The .fsx script alternative will not be covered.

What will we need?

*Azurite is an Azure storage emulator - one of the functions we're about to create uses TimerTrigger and will need this to run and debug locally.

We can install all of the prerequisites from commandline. Head to your favorite terminal utility and feed it these commands, line by line:
Windows:

winget install Microsoft.DotNet.SDK.6
winget install Microsoft.VisualStudioCode
winget install Microsoft.AzureFunctionsCoreTools

Linux:

sudo apt update
sudo apt install -y dotnet-sdk-6.0
sudo apt install software-properties-common apt-transport-https wget
wget -q https://packages.microsoft.com/keys/microsoft.asc -O- | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main"
sudo apt install -y code

The following commands are the same for both Windows and Linux. On Windows you might need to restart the terminal for these to work:

code --install-extension Ionide.Ionide-fsharp
code --install-extension ms-azuretools.vscode-azurefunctions
code --install-extension Azurite.azurite

Templates

With all the prerequisites present, we will proceed to install relevant dotnet templates from Nuget:

dotnet new --install Microsoft.Azure.WebJobs.ItemTemplates
dotnet new --install Microsoft.Azure.WebJobs.ProjectTemplates

The dotnet new command can now work with a broader assortment of templates:

Template Name                        Short Name       Language  Tags
-----------------------------------  ---------------  --------  -----------------------------------------------------
BlobTrigger                          blob             [C#],F#   Azure Function/Trigger/Blob
CosmosDBTrigger                      cosmos,CosmosDB  [C#],F#   Azure Function/Trigger/Cosmos DB
DurableFunctionsEntityOrchestration  durableentity    [C#]      Azure Function/Durable Functions Entity Orchestration
DurableFunctionsOrchestration        durable          [C#]      Azure Function/Durable Functions Orchestration
EventGridTrigger                     eventgrid        [C#]      Azure Function/Trigger/EventGrid
EventHubTrigger                      eventhub         [C#],F#   Azure Function/Trigger/EventHub
HttpTrigger                          http             [C#],F#   Azure Function/Trigger/Http
HttpTriggerWithOpenAPI               httpOpenAPI      [C#]      Azure Function/Trigger/Http
IotHubTrigger                        iothub           [C#]      Azure Function/Trigger/IotHub
KafkaOutput                          kafkao           [C#]      Azure Function/Ouput/Kafka
KafkaTrigger                         kafka            [C#]      Azure Function/Trigger/Kafka
QueueTrigger                         queue            [C#]      Azure Function/Trigger/Storage Queue
RabbitMQTrigger                      rqueue           [C#]      Azure Function/Trigger/RabbitMQ Queue
SendGrid                             sendgrid         [C#]      Azure Function/Ouput/SendGrid
ServiceBusQueueTrigger               squeue           [C#]      Azure Function/Trigger/Service Bus/Queue
ServiceBusTopicTrigger               stopic           [C#]      Azure Function/Trigger/Service Bus/Topic
SignalRTrigger                       signalr          [C#]      Azure Function/Trigger/Http/SignalR
SqlInputBinding                      sqlinput         [C#]      Azure Function/Input/SQL
SqlOutputBinding                     sqloutput        [C#]      Azure Function/Output/SQL
TimerTrigger                         timer            [C#],F#   Azure Function/Trigger/Timer
Azure Functions                      func             [C#],F#   Azure Functions/Solution

Project Files

dotnet new func --language F# --name FSharpFuncApp

This gives us an .fsproj to hold all our functions. Visual Studio and Visual Studio Code (with the Azure Functions extension) can open it as Azure Functions project type, targeting net6.0 and Azure Function runtime v4, which fills us with the confidence of being up with the times. Listing the directory contents, we can see the template went the proverbial extra mile. It has given us a bonus .gitignore file, which has the right things in the right place, for the most part. We will want to tweak it later.

.gitignore
FSharpFuncApp.fsproj
host.json
local.settings.json

We are now ready to create our first function within this project:

dotnet new http --language F# --name Greet

Visual Studio Code

Launch Visual Studio Code in the current folder:

code .

The Azure Functions Extension should detect an Azure Function Project and offer to initialise for optimal use. Answer Yes to the prompt.
We now need to add the .fs file to the .fsproj file manually. Switch to the Explorer tab (Ctrl+Shift+E) and open the FSharpFuncApp.fsproj for editing. Let's put the .fs file (and any other we might add later) in a separate own <ItemGroup> just before the </Project> closing tag:

  <ItemGroup>
    <Compile Include="Greet.fs" />
  </ItemGroup>
</Project>

While here, we also need to make the following changes, otherwise these two files will not copy to the debug folder and local debug attempts will fail:

<None Update="host.json">             ->   <Content Include="host.json">
<None Update="local.settings.json">   ->   <Content Include="local.settings.json">

Let's not forget to change the closing tags (do this for both nodes):

</None>   ->   </Content>

Now open the .gitignore file for editing.

## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.

# Azure Functions localsettings file
local.settings.json

# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates

Let's delete or comment out the line with local.settings.json, as we will need it included in version control. This also means that from now on we must keep in mind not to store any secrets in it.

At this point, everything should be ready. We can hit F5 to debug. The Azure Functions runtime will take a few moments to launch.

Azure Functions Core Tools
Core Tools Version:       4.0.4915 Commit hash: N/A  (64-bit)
Function Runtime Version: 4.14.0.19631

[2023-02-05T16:16:43.897Z] Csproj not found in C:\sandbox\azFunc\bin\Debug\net6.0 directory tree. Skipping user secrets file configuration.

Functions:

        Greet: [GET,POST] http://localhost:7071/api/Greet

For detailed output, run func with --verbose flag.

We can Ctrl+click the function's Url to open it in a browser tab.

This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.

If we edit the Url to add a query parameter and refresh...

http://localhost:7071/api/Greet   ->   http://localhost:7071/api/Greet?name=Dave

...we get a personalised response.

Hello, Dave. This HTTP triggered function executed successfully.

Add another function

Stop the debug run and head back to the terminal. Let's create another function, one of a different type.

dotnet new timer --language F# --name Refresh

Notice that as we try to create multiple functions in the same folder, we are told 4 of the files generated with the first function will get overwritten, and that it will only happen if we include a --force switch with the command.

Creating this template will make changes to existing files:
  Overwrite   ./function.json
  Overwrite   ./metadata.json
  Overwrite   ./readme.md
  Overwrite   ./sample.dat

Rerun the command and pass --force to accept and create.

Here, we do as we're told:

dotnet new timer --language F# --name Refresh --force

Do not worry about the overwritten files. They contain information that will be picked up from the attributes attached to the function definitions in the code files.

[<FunctionName("Refresh")>]
    let run ([<HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)>]req: HttpRequest) (log: ILogger) =
        async {

Let's add the new Refresh.fs to the .fsproj, just like we did with our first function.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.1.1" />
  </ItemGroup>
  <ItemGroup>
    <Content Include="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
    <Content Include="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </Content>
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Greet.fs" />
    <Compile Include="Refresh.fs" />
  </ItemGroup>
</Project>

Storage Emulator

Back In VS Code, in the status bar we can see three buttons:

[Azurite Table Service] [Azurite Queue Service] [Azurite Blob Service]

Only one of them, the Blob Service, needs to be started for the TimerTrigger function to run without errors. Click it to start. Then hit F5 to debug again.

Function Runtime Version: 4.14.0.19631

[2023-02-05T16:56:23.836Z] Csproj not found in C:\sandbox\azFunc\bin\Debug\net6.0 directory tree. Skipping user secrets file configuration.

Functions:

        Greet: [GET,POST] http://localhost:7071/api/Greet

        Refresh: [GET,POST] http://localhost:7071/api/Refresh

For detailed output, run func with --verbose flag.

We can see that the second function was successfully integrated into the project.

Deployment

In VS Code, press Ctrl+Shift+P and search for the Azure: Sign In command.
After you complete the sign-in process in the browser, find and execute another command, this time Azure Functions: Create Function App in Azure....
In the next 3 prompts, you will be asked to provide a unique function name, runtime stack and location to deploy to.
When the provisioning completes, proceed with the next command: Azure Functions: Deploy to Function App.... It will prompt you for a resource, and should offer you a list. Choose the one we created in the previous step.
At this point you should receive a warning that The AzureWebJobsStorage setting in the "local.settings.json" does not match the remote application setting. Make sure you hit Skip at this stage.
If all went well, you should be able to interact with the Greet function on its public Url. In the Azure section of VS Code, if you navigate to Resources > <Subscription> > Function App > <ResourceName> > Functions, you will see a list of deployed functions. Right-click on the Greet function, then on Copy Function Url and paste it into your browser.

Conclusion

As we've seen, the experience of using Azure Functions with F# is still possible, and still a bit hacky.

Sources

This post was inspired and informed by two blog posts by Aaron Powell and Brian Vanderwal respectively.