Azure Functions fully supports F#, which raises the question of why we haven’t one for functional development. The reason is that there hasn’t been enough development done in the functional paradigm.
Giraffe meets Arcus
For developing Web APIs and Azure Functions HTTP triggers in F#, there’s a commonly used library called ‘Giraffe’ that helps overcome the tedious OOP-restrictions of the ASP.NET Core framework. The purpose of this post is not to explain Giraffe, as you can read their thorough documentation for more information on this framework.
All Giraffe needs is a reference in the Startup.fs
. Besides that, all of the Arcus registrations look almost the same in the official template:
module Startup | |
open Giraffe | |
open Giraffe.Serialization | |
open Microsoft.Azure.Functions.Extensions.DependencyInjection | |
open Microsoft.Extensions.Configuration | |
open Microsoft.Extensions.DependencyInjection | |
open Microsoft.Extensions.Hosting | |
open Newtonsoft.Json | |
open Newtonsoft.Json.Converters | |
open Serilog | |
open Serilog.Events | |
open Serilog.Configuration | |
type Startup () = | |
inherit FunctionsStartup () | |
// This method gets called by the runtime. Use this method to configure the app configuration. | |
override __.ConfigureAppConfiguration (builder) = | |
builder.ConfigurationBuilder.AddEnvironmentVariables() |> ignore | |
// This method gets called by the runtime. Use this method to add services to the container. | |
// For more information on how to configure your application, visit https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection | |
override __.Configure (builder: IFunctionsHostBuilder) = | |
let settings = JsonSerializerSettings() in | |
settings.NullValueHandling <- NullValueHandling.Ignore; | |
settings.Converters.Add(StringEnumConverter()) | |
builder.Services.AddSingleton<IJsonSerializer>(NewtonsoftJsonSerializer(settings)) | |
.AddGiraffe() |> ignore | |
let config = builder.GetContext().Configuration | |
builder.ConfigureSecretStore (fun stores -> | |
#if DEBUG | |
stores.AddConfiguration (config) |> ignore | |
#endif | |
stores.AddEnvironmentVariables () |> ignore) |> ignore | |
let instrumentationKey = config.GetValue("APPLICATIONINSIGHTS_INSTRUMENTATIONKEY") | |
let configuration = | |
LoggerConfiguration() | |
.MinimumLevel.Debug() | |
.MinimumLevel.Override("Microsoft", LogEventLevel.Information) | |
.Enrich.FromLogContext() | |
.Enrich.WithComponentName("Azure HTTP Trigger") | |
.Enrich.WithVersion() | |
.WriteTo.Console() | |
.WriteTo.AzureApplicationInsights(instrumentationKey) | |
builder.Services.AddLogging(fun logging -> | |
logging.AddSerilog(configuration.CreateLogger(), dispose=true) |> ignore) |> ignore | |
[<assembly: FunctionsStartup(typeof<Startup>)>] | |
do () |
Note that with Giraffe, we can only specify the JSON settings once, and then the input/output gets adapted directly.
The actual Azure Function HTTP trigger can be found in the Run.fs
file. I’ve already added some starter functions so you can get the idea of a simple Giraffe Azure Functions setup. In the next steps, we’ll look at how we can introduce the boilerplate code that you find in the official Arcus Azure Functions HTTP trigger project template.
module Run | |
open System | |
open System.Threading.Tasks | |
open FSharp.Control.Tasks.V2 | |
open Giraffe | |
open Microsoft.AspNetCore.Http | |
open Microsoft.Azure.WebJobs | |
open Microsoft.Azure.WebJobs.Extensions.Http | |
open Microsoft.Extensions.Logging | |
let app = | |
choose [ POST >=> text "Hello world!" ] | |
let errorHandler (ex : exn) (logger : ILogger)= | |
logger.LogCritical (ex, ex.Message) | |
clearResponse >=> ServerErrors.INTERNAL_ERROR "Could not process the current request due to an unexpected exception" | |
let actionResult f = | |
{ new IActionResult with | |
member _.ExecuteResultAsync (ctx) = f ctx :> Task } | |
[<FunctionName "order">] | |
let run ([<HttpTrigger (AuthorizationLevel.Anonymous, "post", Route = "v1/order")>] req : HttpRequest, context : ExecutionContext, logger : ILogger) = | |
logger.LogInformation("F# HTTP trigger 'order' function processed a request"); | |
let func = Some >> Task.FromResult | |
actionResult <| fun ctx -> task { | |
try return! app func ctx.HttpContext :> Task | |
with exn -> return! errorHandler exn logger func ctx.HttpContext :> Task } |
Request restrictions
The Azure Functions HTTP trigger template contains some request restrictions to make sure that the HTTP request is valid. Normally, this is done in some middleware, but since we’re in Azure Functions (and not yet in out-of-process functions), we will assume that there’s no middleware available.
The Arcus project template uses a re-usable base class to provide you with some functionality to restrict the request. Fortunately, with Giraffe we can simply use functions for this.
I’ve created some functions in the Arcus style on a personal GitHub repo which can directly be referenced in F# paket projects. These functions will help with making sure that the incoming request has the correct metadata information before we do any binding or serialization. Here are some examples added to the Azure Function:
let app = | |
choose [ | |
POST >=> haveContentType "application/json" | |
>=> mustAccept "application/json" | |
>=> text "Hello world!" | |
RequestErrors.methodNotAllowed (text "Could not process the request as only POST requests are allowed") ] |
HTTP correlation
We use the HTTP correlation functionality from the Arcus Web API library to handle the correlation in the template. We can also add it in our functional example, but we need to make some alterations to easily integrate it into our Giraffe pipeline.
open Arcus.WebApi.Logging.Correlation | |
let tryCorrelate = | |
fun next (ctx : HttpContext) -> | |
let correlation = ctx.RequestServices.GetRequiredService<HttpCorrelation> () | |
let error = ref "" | |
if correlation.TryHttpCorrelate error then next ctx | |
else RequestErrors.badRequest (text !error) earlyReturn ctx |
The Arcus correlation is now fully functional and can be added to our Giraffe pipeline:
let app = | |
choose [ | |
POST >=> haveContentType "application/json" | |
>=> mustAccept "application/json" | |
>=> tryCorrelate | |
>=> text "Hello world!" | |
RequestErrors.methodNotAllowed (text "Could not process the request as only POST requests are allowed") ] |
JSON binding
To deserialize the request body, we provided some binding methods in the base class. With a concrete JSON exception type, we’ve handled any problems during deserialization. In Giraffe, the same sort of thing can be accomplished. We should make sure that we also verify any validation attributes on the type we’re trying to bind. All this functionality is also available in F#.
First things first, let’s start with our JSON model. Luckily, we can simply use F# records for this:
type OrderJson = | |
{ [<Required>] Id : string | |
[<Required; MaxLength(100)>] ArticleNumber : string | |
[<Required; DataType(DataType.DateTime)>] Scheduled : DateTimeOffset } |
To deserialize, we can use Giraffe’s .BindJsonAsync
. Since we also need some additional error handling, let’s wrap these in a separate higher-order function to take in the successful flow:
let tryBindJson<'T> (f : 'T -> HttpHandler) : HttpHandler = | |
fun (next : HttpFunc) (ctx : HttpContext) -> | |
task { | |
try let! model = ctx.BindJsonAsync<'T> () | |
match Option.ofObj (box model) with | |
| Some v -> return! f (v :?> 'T) next ctx | |
| None -> return! RequestErrors.badRequest (text "Could not process the request due to an JSON deserialization failure") earlyReturn ctx | |
with :? JsonException as ex -> | |
let logger = ctx.GetLogger<'T> () | |
logger.LogError (ex, "Could not process the request due to an JSON deserialization failure: {description}", ex.Message) | |
return! RequestErrors.badRequest (text "Could not process the request due to an JSON deserialization failure") earlyReturn ctx } |
To validate the model, we can use the Giraffe’s IModelValidation
interface. With a little tweak, we can let it validate our ASP.NET Core validation attributes:
open System.Collections.ObjectModel | |
open System.ComponentModel.DataAnnotations | |
type OrderJson = | |
{ [<Required>] Id : string | |
[<Required; MaxLength(100)>] ArticleNumber : string | |
[<Required; DataType(DataType.DateTime)>] Scheduled : DateTimeOffset } with | |
interface IModelValidation<OrderJson> with | |
member this.Validate () = | |
let errors = Collection<ValidationResult> () | |
if Validator.TryValidateObject (this, ValidationContext(this), errors) | |
then Result.Ok this | |
else RequestErrors.badRequest (json errors) |> Result.Error |
The validateModel
Giraffe pipeline function (f:('T -> HttpHandler) -> model:'T -> HttpHandler
) will take care of this. In combination with our previously added JSON deserialization function, our Giraffe pipeline now looks like this:
let app = | |
choose [ | |
POST >=> haveContentType "application/json" | |
>=> mustAccept "application/json" | |
>=> tryCorrelate | |
>=> tryBindJson<OrderJson> (validateModel json) | |
RequestErrors.methodNotAllowed (text "Could not process the request as only POST requests are allowed") ] |
Note that the validateModel
function takes in a result function where the successful validation result should be sent to (like our tryBindJson
function does). I’ve chosen here to take the same approach as with the Arcus template, and have sent back the order with Giraffe’s json
function.
Domain Modelling
As a bonus, try to introduce a domain model with some domain model requirements, and to parse that model. With my personal FPrmitive package, this can be done rather easily in the same functional manner. For more information on this package and examples on how this library can be used, see my other blog posts.
Conclusion
In this post, we’ve looked at some alternative ways to provide users with a starter project that already does much of the highly-needed boilerplate code. We’ve seen that with Giraffe and the built-in F# support for Azure Functions, we can accomplish a lot.
Hopefully, you will consider F# as an option in your next project, because it’s definitely worth a look. Combining C# and F# is even better.
Thanks for reading!
Subscribe to our RSS feed