Let’s see how F# language features make it simple to create a unit conversion service and deploy it to Azure Functions.
Q: Who made this Workshop?
A: Brett Rowberry, who works at ExxonMobil
Q: When was this workshop first given?
A: Friday September 27th, 2019 at Open F# in San Francisco
Q: How long does this workshop take?
A: 110 minutes
Q: What prerequisites will I need?
A:
- An Azure account - get a year for free here
- git (to clone this repository)
- Visual Studio Code
- Ionide extension (install through
Extensions
in Visual Studio Code`) - Azure Functions extension
- Node.js 8.5+ (used to install Azure Functions Core Tools on Windows)
- .NET Core 2.1 SDK - needed for Azure Functions Core Tools
- See which versions you have How to remove the .NET Core Runtime and SDK
dotnet --list-sdks
- See which versions you have How to remove the .NET Core Runtime and SDK
- Azure Functions Core Tools
Q: Why Azure Functions?
A:
- Why serverless?
- Potentially less expensive
- Potentially a simpler programming model
- Why Azure Functions and not AWS Lambda or other serverless offerings?
- I use Azure at work
Q: What are the programming/deployment models supported by Azure Functions?
A:
- Script
- Compiled (what we'll use in this workshop)
- Container
There isn't an official F# template at the moment, so we'll start with a C# tutorial.
- Open the
module1
directory in Visual Studio Code - Navigate to Create your first function using Visual Studio Code
- Here are the main sections that we will go over together:
- Prerequisites
- Create your Functions project using Visual Studio Code's Command Palette
- Accept all the defaults
- Run the function locally and call it
- Publish the project to Azure using Visual Studio Code's Command Palette
- Use the basic publish option (not advanced),
Azure Functions: Deploy to Function App...
- Name your app
module1<yourname>
- Use the Azure region closest to you. We'll use
West US
region since we're in San Francisco.
- Use the basic publish option (not advanced),
- Call the deployed API
- Open the
module2
directory in Visual Studio Code - Create the C# project again, this time using the Azure Functions extension GUI
- The button looks like a folder with a lightning bolt and the tooltip says
Create New Project...
- Change the function name to
HttpTriggerFSharp
- Accept other defaults
- The button looks like a folder with a lightning bolt and the tooltip says
- Navigate to Azure Functions With F#. Thank you Aaron Powell for your post and for allowing us to use it in this workshop!
- Copy the code to the source file and change the extension from
.cs
to.fs
(Ionide might look really upset at the file for a while, don't worry!) - Change the extension of the project file from
.csproj
to.fsproj
- In the
.fsproj
file below the first<ItemGroup>
section paste
- Copy the code to the source file and change the extension from
<ItemGroup>
<Compile Include="HttpTriggerFSharp.fs" />
</ItemGroup>
- Run it to make sure it works
POST
s aren't very fun to test. Let's change the function to aGET
that uses query parameters like in Module 1.- Paste over the code with
namespace Company.Function
open Microsoft.Azure.WebJobs
open Microsoft.Azure.WebJobs.Extensions.Http
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Logging
open Microsoft.AspNetCore.Mvc
open System
open Microsoft.Extensions.Primitives
module HttpTrigger =
[<FunctionName("HttpTrigger")>]
let Run([<HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = null)>]
req: HttpRequest,
log: ILogger)
=
let stringValues = req.Query.Item "name"
if StringValues.IsNullOrEmpty stringValues
then
log.LogInformation("no name was passed")
BadRequestObjectResult("Include a 'name' as a query string.") :> ActionResult
else
let name = stringValues.[0]
log.LogInformation(sprintf "name was '%s'" name)
OkObjectResult(name) :> ActionResult
- Run the function locally and call it.
- Note that we switched the authorization from
Function
toAnonymous
- Note that we switched the authorization from
- Publish the project to Azure using the GUI
- Choose the advanced publish option
- Name your app
module2<yourname>
- Choose
Windows
instead ofLinux
- Choose
.NET
for runtime - Accept all other defaults
- There will be a prompt to stream logs, accept it
- Call your app, inspect the logs
- Navigate to https://portal.azure.com
- Select your Function App
- Disable and reenable the app
- Run a test
- Open the
module3
directory in Visual Studio Code - Create the same project as in Module 2
- Name the app
UnitConversionAPI
- This time we'll use route parameters instead of query parameters
- Here's the code:
- Name the app
namespace API
open System
open Microsoft.AspNetCore.Http
open Microsoft.Azure.WebJobs
open Microsoft.Azure.WebJobs.Extensions.Http
open Microsoft.Extensions.Logging
module Length =
[<FunctionName("Length")>]
let Run([<HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = "Length/{source}/{target}/{input}")>]
req: HttpRequest,
source: string,
target: string,
input: string,
log: ILogger)
=
let inputs = String.Join("|", source, target, input)
log.LogInformation(sprintf "Inputs: '%s'" inputs)
inputs
- Run the function locally and call it
- Now that we have a working app, let's implement the conversion logic. Add a file above the existing file named
Length.fs
namespace UnitConversion
open System.Collections.Generic
open System.Linq
open System
module Length =
let private lengthsAndFactors =
let fsharpDict =
dict [
"meter", 1.0
"millimeter", 1e-3
"kilometer", 1e3 ]
Dictionary<string, float>(fsharpDict)
let private tryGetUnitFactor name =
match lengthsAndFactors.TryGetValue name with
| true, factor -> Some factor
| _ -> None
let private lengths =
let lengths = lengthsAndFactors.Keys.ToArray()
String.Join(", ", lengths)
let convert source target input =
match (tryGetUnitFactor source, tryGetUnitFactor target) with
| None, Some _ ->
sprintf "Length unit '%s' not found. Try %s." source lengths |> Error
| Some _, None ->
sprintf "Length unit '%s' not found. Try %s." target lengths |> Error
| None, None ->
sprintf "Length units '%s' and '%s' not found. Try %s." source target lengths |> Error
| Some s, Some t ->
input * s / t |> Ok
- Change your functions file to be:
namespace API
open Microsoft.Azure.WebJobs
open Microsoft.Azure.WebJobs.Extensions.Http
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Logging
open Microsoft.AspNetCore.Mvc
open System
open UnitConversion
module LengthAPI =
open UnitConversion.Length
[<FunctionName("LengthAPI")>]
let Run([<HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = "length/{source}/{target}/{input}")>]
req: HttpRequest,
source: string,
target: string,
input: float,
log: ILogger)
=
let inputs = String.Join("|", source, target, input)
log.LogInformation(sprintf "Inputs: '%s'" inputs)
match Length.convert source target input with
| Ok result ->
log.LogInformation (sprintf "Conversion result: %f" result)
OkObjectResult result :> ActionResult
| Error msg ->
NotFoundObjectResult msg :> ActionResult
- Run the function locally and call it
- Publish the project to Azure and call it
- Name your app
module3<yourname>
- Name your app
- F# in 10 Minutes or Less | Precompiled Azure Functions V2 in Visual Studio Code
- Using F# to write serverless Azure functions a little old
- Work with Azure Functions Core Tools
- HTTP routing
- Precompiled Azure Functions in F#
- Available templates for dotnet new - maybe you can make your own!
- Giraffe.AzureFunctions
- Azure F#unctions
- DurableFunctions.FSharp
- Secure an Azure Function App with Azure Active Directory
- Tic-Tac-Toe with F#, Azure Functions, HATEOAS and Property-Based Tests
- Introducing Saturn on Functions
- fantomas-ui
- F# Yoga Class Booking System
- Azure Durable Functions in F#
- azure-functions-durable-extension/samples/fsharp
- A Fairy Tale of F# and Durable Functions
- Creating custom bindings for Azure Functions
- azure-functions-fsharp-examples