/Falco

A functional-first toolkit for building brilliant ASP.NET Core applications using F#.

Primary LanguageF#Apache License 2.0Apache-2.0

Falco

NuGet Version Build Status

Falco is a toolkit for building functional-first, fast and fault-tolerant web applications using F#.

  • Built upon the high-performance primitives of ASP.NET Core.
  • Optimized for building HTTP applications quickly.
  • Seamlessly integrates with existing .NET Core middleware and frameworks.

Key features

Design Goals

  • Aim to be very small and easily learnable.
  • Should be extensible.
  • Should provide a toolset to build a working end-to-end web application.

Quick Start - Hello World 3 ways

Create a new F# web project:

dotnet new web -lang F# -o HelloWorldApp

Install the nuget package:

dotnet add package Falco

Remove the Startup.fs file and save the following in Program.fs:

module HelloWorld.Program

open Falco
open Falco.Markup
open Falco.Routing

let endpoints = 
    [            
        get "/json" 
            (Response.ofJson {| Message = "Hello from /json" |})

        get "/html" 
            (Response.ofHtml (Templates.html5 "en" [] [ Elem.h1 [] [ Text.raw "Hello from /html" ] ]))

        get "/" 
            (Response.ofPlainText "Hello from /")
    ]

[<EntryPoint>]
let main args =            
    Host.startWebHostDefault args endpoints
    0

Run the application:

dotnet run HelloWorldApp

There you have it, an industrial-strength "hello world 3 ways" web app, achieved using primarily base ASP.NET Core libraries. Pretty sweet!

Sample Applications

Code is always worth a thousand words, so for the most up-to-date usage, the /samples directory contains a few sample applications.

Sample Description
HelloWorld A basic hello world app
Blog A basic markdown (with YAML frontmatter) blog

Request Handling

The HttpHandler type is used to represent the processing of a request. It can be thought of as the eventual (i.e. asynchronous) completion of and HTTP request processing, defined in F# as: HttpContext -> Task. Handlers will typically involve some combination of: route inspection, form/query binding, business logic and finally response writing. With access to the HttpContext you are able to inspect all components of the request, and manipulate the response in any way you choose.

Basic request/resposne handling is divided between the aptly named Request and Response modules.

  • Plain Text responses
let textHandler : HttpHandler =
    Response.ofPlainText "Hello World"
  • HTML responses
let htmlHandler : HttpHandler =
    let doc = 
        Elem.html [ Attr.lang "en" ] [
                Elem.head [] [                    
                        Elem.title [] [ Text.raw "Sample App" ]                                                            
                    ]
                Elem.body [] [                     
                        Elem.main [] [
                                Elem.h1 [] [ Text.raw "Sample App" ]
                            ]
                    ]
            ] 

    Response.ofHtml doc
  • JSON responses

IMPORTANT: This handler will not work with F# options or unions, since it uses the default System.Text.Json.JsonSerializer. See JSON section below for further information.

type Person =
    {
        First : string
        Last  : string
    }

let jsonHandler : HttpHandler =
    { First = "John"; Last = "Doe" }
    |> Response.ofJson
  • Set the status code of the response
let notFoundHandler : HttpHandler =
    Response.withStatusCode 404
    >> Response.ofPlainText "Not found"
  • Redirect (301/302) Response (boolean param to indicate permanency)
let oldUrlHandler : HttpHandler =
    Response.redirect "/new-url" true
  • Accessing route parameters.
    • The following function defines an HttpHandler which uses the route value called "name" and the built-in Response.ofPlainText handler to return a plain-text greeting to the client:
let helloHandler : HttpHandler =    
    Request.mapRoute 
        (fun route -> route.["name"] |> sprintf "hi %s") 
        Response.ofPlainText

Routing

The breakdown of Endpoint Routing is simple. Associate a a specific route pattern (and optionally an HTTP verb) to an HttpHandler which represents the ongoing processing (and eventual return) of a request.

Bearing this in mind, routing can practically be represented by a list of these "mappings" known in Falco as an HttpEndpoint which bind together: a route, verb and handler.

let helloHandler : HttpHandler = // ...

let greetingHandler name : HttpHandler = // ...

let loginHandler : HttpHandler = // ...

let loginSubmitHandler : HttpHandler = // ...  

let endpoints : HttpEndpoint list = 
  [
    // a basic GET handler
    get "/hello/{name:alpha}" 
        helloHandler    

    // map route then handle
    get "/greet/{name:alpha}"
        (Request.mapRoute (fun r -> r.["name"] |> sprintf "Hi %s") greetingHandler)

    // multi-method endpoint
    all "/login"              
        [ 
            handle POST loginSubmitHandler        
            handle GET  loginHandler
        ]
  ]

Host

Kestrel is the web server at the heart of ASP.NET. It's a performant, secure and maintained by incredibly smart people. If you're looking to get a host up and running quickly the Host.startWebHostDefault function will enable everythigng necessary for Falco to work:

[<EntryPoint>]
let main args =        
    Host.startWebHostDefault 
        args 
        [
            // Endpoints go here
        ]
    0

Should you wish to fully customize your host instance the Host.startWebHost exposes the IWebHostBuilder which enables full customization. For a full example, see the Blog sample.

// Logging
let configureLogging 
    (log : ILoggingBuilder) =
    log.SetMinimumLevel(LogLevel.Error)
    |> ignore

// Services
let configureServices 
    (services : IServiceCollection) =
    services.AddRouting()     
            .AddResponseCaching()
            .AddResponseCompression()
    |> ignore

// Middleware
let configure                 
    (endpoints : HttpEndpoint list)
    (app : IApplicationBuilder) = 
            
    app.UseExceptionMiddleware(Host.defaultExceptionHandler)
        .UseResponseCaching()
        .UseResponseCompression()
        .UseStaticFiles()
        .UseRouting()
        .UseHttpEndPoints(endpoints)
        .UseNotFoundHandler(Host.defaultNotFoundHandler)
        |> ignore 

// Web Host
let configureWebHost : ConfigureWebHost =
  fun (endpoints : HttpEndPointList) 
      (host : IWebHostBuilder) ->
      webHost
           .UseKestrel()
           .ConfigureLogging(configureLogging)
           .ConfigureServices(configureServices)
           .Configure(configure endpoints)
           |> ignore

[<EntryPoint>]
let main args =    
    Host.startWebHost 
        args
        configureWebHost
        [
            // Endpoints go here
        ]
    0

Markup

A core feature of Falco is the XML markup module. It can be used to produce any form of angle-bracket markup (i.e. HTML & SVG).

The module is easily extended since creating new tags is simple. An example to render <svg>'s:

let svg (width : float) (height : float) =
    Elem.tag "svg" [
        Attr.create "version" "1.0"
        Attr.create "xmlns" "http://www.w3.org/2000/svg"
        Attr.create "viewBox" (sprintf "0 0 %f %f" width height)
    ]

let path d = Elem.tag "path" [ Attr.create "d" d ] []

let bars =
    svg 384.0 384.0 [
            path "M368 154.668H16c-8.832 0-16-7.168-16-16s7.168-16 16-16h352c8.832 0 16 7.168 16 16s-7.168 16-16 16zm0 0M368 32H16C7.168 32 0 24.832 0 16S7.168 0 16 0h352c8.832 0 16 7.168 16 16s-7.168 16-16 16zm0 0M368 277.332H16c-8.832 0-16-7.168-16-16s7.168-16 16-16h352c8.832 0 16 7.168 16 16s-7.168 16-16 16zm0 0"
        ]

HTML View Engine

Most of the standard HTML tags & attributes have been built into the markup module, which produce objects to represent the HTML node. Nodes are either:

  • Text which represents string values.
  • SelfClosingNode which represent self-closing tags (i.e. <br />).
  • ParentNode which represent typical tags with, optionally, other tags within it (i.e. <div>...</div>).

The benefits of using the Falco markup module as an HTML engine include:

  • Writing your views in plain F#, directly in your assembly.
  • Markup is compiled alongside the rest of your code, leading to improved performance and ultimately simpler deployments.
// Create an HTML5 document using built-in template
let doc = 
    Templates.html5 "en"
        [ Elem.title [] [ Text.raw "Sample App" ] ] // <head></head>
        [ Elem.h1 [] [ Text.raw "Sample App" ] ]    // <body></body>

Since views are plain F# they can easily be made strongly-typed:

type Person =
    {
        First : string
        Last  : string
    }

let doc (person : Person) = 
    Elem.html [ Attr.lang "en" ] [
            Elem.head [] [                    
                    Elem.title [] [ Text.raw "Sample App" ]                                                            
                ]
            Elem.body [] [                     
                    Elem.main [] [
                            Elem.h1 [] [ Text.raw "Sample App" ]
                            Elem.p  [] [ Text.raw (sprintf "%s %s" person.First person.Last)]
                        ]
                ]
        ]

Views can also be combined to create more complex views and share output:

let master (title : string) (content : XmlNode list) =
    Elem.html [ Attr.lang "en" ] [
            Elem.head [] [                    
                    Elem.title [] [ Text.raw "Sample App" ]                                                            
                ]
            Elem.body [] content
        ]

let divider = 
    Elem.hr [ Attr.class' "divider" ]

let homeView =
    [
        Elem.h1 [] [ Text.raw "Homepage" ]
        divider
        Elem.p  [] [ Text.raw "Lorem ipsum dolor sit amet, consectetur adipiscing."]
    ]
    |> master "Homepage" 

let aboutView =
    [
        Elem.h1 [] [ Text.raw "About" ]
        divider
        Elem.p  [] [ Text.raw "Lorem ipsum dolor sit amet, consectetur adipiscing."]
    ]
    |> master "About Us"

Model Binding

Binding at IO boundaries is messy, error-prone and often verbose. Reflection-based abstractions tend to work well for simple use cases, but quickly become very complicated as the expected complexity of the input rises. This is especially true for an algebraic type system like F#'s. As such, it is often advisable to take back control of this process from the runtime. An added bonus of doing this is that it all but eliminates the need for [<CLIMutable>] attributes.

We can make this simpler by creating a succinct API to obtain typed values from IFormCollection and IQueryCollection.

Methods are available for all primitive types, and perform case-insenstivie lookups against the collection.

Query Binding

Query binding is enabled by the StringCollectionReader, an abstraction intended to make it easier to work with the query collection.

type Person = { FirstName : string; LastName : string }

let mapQueryHandler : HttpHandler =    
    Request.mapQuery
        (fun rd -> { 
            FirstName = rd.GetString "FirstName" "John" // Get value or return default value
            LastName = rd.GetString "LastName" "Doe" 
        })
        Response.ofJson 

let bindQueryHandler : HttpHandler = 
    Request.bindQuery 
        (fun rd -> 
            match rd.TryGetString "FirstName", rd.TryGetString "LastName" with
            | Some f, Some l -> Ok { FirstName = f; LastName = l }
            | _  -> Error {| Message = "Invalid query string" |})
        Response.ofJson // handle Ok
        Response.ofJson // handle Error

let manualQueryHandler : HttpHandler =
    fun ctx ->
        let query = Request.getQuery ctx
        
        let person = 
            { 
                FirstName = query.GetString "FirstName" "John" // Get value or return default value
                LastName = query.GetString "LastName" "Doe" 
            }

        Response.ofJson person ctx

Form Binding

Form value binding is enabled by the FormCollectionReader, which inherits from StringCollectionReader thus sharing it's API. Because of this you'll notice from the example below, that the code used to map and bind query values and forms is virtually identical, exception being the manual handler since form IO is asynchronous.

Note the addition of Request.mapFormSecure and Request.bindFormSecure which will automatically validate CSRF tokens for you.

type Person = { FirstName : string; LastName : string }

let mapFormHandler : HttpHandler =    
    Request.mapForm
        (fun rd -> { 
            FirstName = rd.GetString "FirstName" "John" // Get value or return default value
            LastName = rd.GetString "LastName" "Doe" 
        })
        Response.ofJson 

let mapFormSecureHandler : HttpHandler =    
    Request.mapFormSecure
        (fun rd -> { 
            FirstName = rd.GetString "FirstName" "John" // Get value or return default value
            LastName = rd.GetString "LastName" "Doe" 
        })
        Response.ofJson 
        (Response.withStatusCode 400 >> Response.ofEmpty)

let bindFormHandler : HttpHandler = 
    Request.bindForm 
        (fun rd -> 
            match rd.TryGetString "FirstName", rd.TryGetString "LastName" with
            | Some f, Some l -> Ok { FirstName = f; LastName = l }
            | _  -> Error {| Message = "Invalid form data" |})
        Response.ofJson // handle Ok
        Response.ofJson // handle Error

let bindFormSecureHandler : HttpHandler = 
    Request.bindFormSecure
        (fun rd -> 
            match rd.TryGetString "FirstName", rd.TryGetString "LastName" with
            | Some f, Some l -> Ok { FirstName = f; LastName = l }
            | _  -> Error {| Message = "Invalid form data" |})
        Response.ofJson // handle Ok
        Response.ofJson // handle Error
        (Response.withStatusCode 400 >> Response.ofEmpty)

let manualFormHandler : HttpHandler =
    fun ctx -> task {
        let! query = Request.getForm ctx
        
        let person = 
            { 
                FirstName = query.GetString "FirstName" "John" // Get value or return default value
                LastName = query.GetString "LastName" "Doe" 
            }

        return! Response.ofJson person ctx
    }        

JSON Binding

Bind JSON using the built-in System.Text.Json API.

type Person = { FirstName : string; LastName : string }

let jsonBindHandler : HttpHandler =    
    Request.bindJson
        (fun person -> Response.ofPlainText (sprintf "hello %s %s" person.FirstName person.LastName))
        (fun error -> Response.withStatusCode 400 >> Response.ofPlainText (sprintf "Invalid JSON: %s" error))

Dynamic value binding

In the case where you don't care about gracefully handling non-existence. Or, you are certain values will be present, the dynamic operator ? can be useful for both the StringCollectionReader and FormCollectionReader:

Use of the ? dynamic operator performs case-insenstive lookups against the collection.

let parseQueryHandler : HttpHandler =
    fun ctx ->
        let query = Request.getQuery ctx

        // dynamic operator also case-insensitive
        let firstName = query?FirstName.AsString() // string -> string
        let lastName  = query?LastName.AsString()  // string -> string
        let age       = query?Age.AsInt16()        // string -> int16

        // Rest of handler ...

Authentication

ASP.NET Core has amazing built-in support for authentication. Review the docs for specific implementation details. Falco optionally (open Falco.Auth) includes some authentication utilites.

To use the authentication helpers, ensure the service has been registered (AddAuthentication()) with the IServiceCollection and activated (UseAuthentication()) using the IApplicationBuilder.

  • Prevent user from accessing secure endpoint:
open Falco.Security

let secureResourceHandler : HttpHandler =
    fun ctx ->
        let respondWith =
            match Auth.isAuthenticated ctx with
            | false -> Response.redirect "/forbidden" false
            | true  -> Response.ofPlainText "hello authenticated user"

        respondWith ctx
  • Prevent authenticated user from accessing anonymous-only end-point:
open Falco.Security
 
let anonResourceOnlyHandler : HttpHandler =
    fun ctx ->
        let respondWith =
            match Auth.isAuthenticated ctx with
            | true  -> Response.redirect "/forbidden" false
            | false -> Response.ofPlainText "hello anonymous"

        respondWith ctx
  • Allow only user's from a certain group to access endpoint"
open Falco.Security

let secureResourceHandler : HttpHandler =
    fun ctx ->
        let isAuthenticated = Auth.isAuthenticated ctx
        let isAdmin = Auth.isInRole ["Admin"] ctx
        
        let respondWith =
            match isAuthenticated, isAdmin with
            | true, true -> Response.ofPlainText "hello admin"
            | _          -> Response.redirect "/forbidden" false

        respondWith ctx
  • End user session (sign out):
open Falco.Security

let logOut : HttpHandler =         
    fun ctx -> 
        (Auth.signOutAsync Auth.authScheme ctx).Wait()
        Response.redirect Urls.``/login`` false ctx

// OR using task {}
let logOut : HttpHandler =         
    fun ctx -> task {
        do! Auth.signOutAsync Auth.authScheme ctx
        do! Response.redirect Urls.``/login`` false ctx
    }

Security

Cross-site scripting attacks are extremely common, since they are quite simple to carry out. Fortunately, protecting against them is as easy as performing them.

The Microsoft.AspNetCore.Antiforgery package provides the required utilities to easily protect yourself against such attacks.

Falco provides a few handlers via Falco.Security.Xss:

To use the Xss helpers, ensure the service has been registered (AddAntiforgery()) with the IServiceCollection and activated (UseAntiforgery()) using the IApplicationBuilder.

open Falco.Security 

let formView (token : AntiforgeryTokenSet) = 
    html [] [
            body [] [
                    form [ _method "post" ] [
                            // using the CSRF HTML helper
                            Xss.antiforgeryInput token
                            input [ _type "submit"; _value "Submit" ]
                        ]                                
                ]
        ]
    
// A handler that demonstrates obtaining a
// CSRF token and applying it to a view
let csrfViewHandler : HttpHandler = 
    fun ctx ->
        let csrfToken = Xss.getToken ctx        
        Response.ofHtml (formView token) ctx
    
// A handler that demonstrates validating
// the requests CSRF token
let isTokenValidHandler : HttpHander =
    fun ctx -> task {
        let! isTokenValid = Xss.validateToken ctx

        let respondWith =
            match isTokenValid with
            | false -> 
                Response.withStatusCode 400 
                >> Response.ofPlainText "Bad request"

            | true ->
                Response.ofPlainText "Good request"                

        return! respondWith ctx
    }

Crytography

Many sites have the requirement of a secure log in and sign up (i.e. registering and maintaining a user's database). Thus, generating strong hashes and random salts is of critical importance.

Falco helpers are accessed by importing Falco.Auth.Crypto.

open Falco.Security

// Generating salt,
// using System.Security.Cryptography.RandomNumberGenerator,
// create a random 16 byte salt and base 64 encode
let salt = Crypto.createSalt 16 

// Generate random int for iterations
let iterations = Crypto.randomInt 10000 50000

// Pbkdf2 Key derivation using HMAC algorithm with SHA256 hashing function
let password = "5upe45ecure"
let hashedPassword = password |> Crypto.sha256 iterations 32 salt

Handling Large Uploads

Microsoft defines large uploads as anything > 64KB, which well... is most uploads. Anything beyond this size, and they recommend streaming the multipart data to avoid excess memory consumption.

To make this process a lot easier Falco exposes an HttpContext extension method TryStreamFormAsync() that will attempt to stream multipart form data, or return an error message indicating the likely problem.

let imageUploadHandler : HttpHandler =
    fun ctx -> task {
        let! form = Request.tryStreamFormAsync()
            
        // Rest of code using `FormCollectionReader`
        // ...
    }

JSON

Included in Falco are basic JSON in/out handlers, Request.bindJsonAsync<'a> and Response.ofJson respectively. Both rely on System.Text.Json, thus without support for F#'s algebraic types. This was done purposefully in support of the belief that JSON in F# should be limited to primitive types only in the form of DTO records.

Why "Falco"?

It's all about Kestrel, a simply beautiful piece of software that has been a game changer for the .NET web stack. In the animal kingdom, "Kestrel" is a name given to several members of the falcon genus, also known as "Falco".

Find a bug?

There's an issue for that.

License

Built with ♥ by Pim Brouwers in Toronto, ON. Licensed under Apache License 2.0.