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?
- .NET 6 SDK
- Visual Studio Code
- Azure Functions Core Tools
- Visual Studio Code Extension for Azure Functions
- Azurite*
*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
, 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.Resources > <Subscription> > Function App > <ResourceName> > Functions
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.