Composable Database Connection Middleware with Giraffe

If someone is new to Giraffe and approaching it with familiarity of the built-in .NET server utilities, then utilizing the compose combinator ( >=>) may take some getting used to. This post will explore using the compose combinator through utilizing it to supply http route handlers with a database connection. We will make some composable middleware to provide a route with either a connection to a reader instance or a writer instance. The middleware will also handle closing the connection when the request has completed.

Connection Utilities

Assuming we already have a simple running Giraffe server, let's make a module that will contain all of our database-related functions. We'll call it DB.fs. The first two functions we'll make will return the connection for a reader instance, and a writer instance.

// DB.fs
module DB

open System.Data
open MySqlConnector

let read () =
    let cs =
        $"server={dbReaderURL};user id={dbUserID};password={dbPassword};port={dbPort}"

    let conn: IDbConnection = new MySqlConnection(cs) :> IDbConnection

    conn.Open()
    conn

let write () =
    let cs =
        $"server={dbWriterURL};user id={dbUserID};password={dbPassword};port={dbPort}"

    let conn: IDbConnection = new MySqlConnection(cs) :> IDbConnection

    conn.Open()
    conn

These functions will create and return a new connection when invoked with read() or write().

Connection Middleware

Now that we have some functions that create the connection, we will make a composable middleware so that our requests can make queries. To do this, we'll add some more things to our DB module (eventually you may want to pull all middleware out into a Middleware module, but we'll keep it simple).

First we will make a class to encapsulate the connection and make it disposable, so that the connection will close when an http request's scope is terminated. Adding to DB.fs...

type DatabaseConnection (connection: IDbConnection) =
    let Connection = connection
    
    member _.GetConnection() =
        Connection
    
    interface IDisposable with
        member _.Dispose() =
            Connection.Close()

Next we can make a function that will create the connection and attach it to the current http context. This is the actual middleware.

let withConnection (connection: unit -> IDbConnection) (next: HttpFunc) (ctx: HttpContext) =
    task {
       use db = new DatabaseConnection(connection())
       let conn = db.GetConnection()
       ctx.Items["db"] <- conn
       return! next ctx
    }

A few things to note in the above. First, notice that the function takes a connection parameter of the type unit -> IDbConnection. This is because we have two possible connections: one for the reader and one for the writer, and we take the function instead of a connection to keep things in scope of the current request. Second, you'll notice that we're attaching the connection to the http context on the line ctx.Items["db"] <- conn. We'll access that later down below. Finally, we return next ctx, passing the request forward to the next middleware or function handler in the assembly line.

Now that we have a withConnection function, lets compose a couple more specific middlewares with that one. These will be for attaching each specific connection to a request. These two go in the same file, just below the code we've already added.

type Middleware = HttpFunc -> HttpContext -> Task<HttpContext option>

let withReader: Middleware =
    read |> withConnection
       
let withWriter: Middleware =
    write |> withConnection

With those two functions, we can now declaratively add either connection to any route. Let's do that now.

Using the Middleware

Assuming that we already have a basic Giraffe server set up with some routes, we can now attach the DB connection to any of these routes as needed. Say we have a GET route which gets all users and it looks like this:

let handlers: HttpHandler =
    choose [ GET
             >=> choose [ route "/users" >=> warbler (fun _ -> handleContext getUsers) ] ]
💡
If you are new to Giraffe, then warbler may look odd. From the Giraffe docs:

"Functions in F# are eagerly evaluated and a normal route will only be evaluated the first time. A warbler will ensure that a function will get evaluated every time the route is hit..."

You can give this individual route access to the reader DB instance like this:

let handlers: HttpHandler =
    choose [ GET
             >=> choose [ route "/users"
                          >=> withReader
                          >=> warbler (fun _ -> handleContext getUsers) ] ]

Or say you know that all GET calls will want the reader instance. In that case you can move the withReader middlware up a level to apply to all GETs.

let handlers: HttpHandler =
    choose [ GET
             >=> withReader
             >=> choose [ route "/users" >=> warbler (fun _ -> handleContext getUsers) ] ]

Similarly, withWriter can be added to any routes that are not readonly.

💡
You can create and chain more middleware in the same way we have done here. For example, you may have, withLogging, isAuthenticated, isAdmin, etc, all chained together as needed using the >=> operator. (It's worth noting that the >=> operator is a custom operator provided by Giraffe, not a native F# operator.)

Accessing the Connection via Http Context

Finally, now that we have the middlware which attaches the connection to the context, and we've applied that middleware to a route, we can access the connection from within the route handler.

To do so we'll pull the DB connection off of the context and downcast it.

let getUsers (ctx: HttpContext) =
    task {
        let conn = ctx.Items["db"] :?> IDbConnection
        let users =
            conn.Query('your SQL here');

        return! ctx.WriteJsonAsync(users)
    }

This is a bit verbose though if we do it over and over again, so let's go back to our DB.fs file and add a utility to make it nicer.

let conn (ctx: HttpContext) =
    ctx.Items["db"] :?> IDbConnection

Import that module into the route handler and now we can clean it up a bit.

let getUsers (ctx: HttpContext) =
    task {
        let users =
            conn(ctx).Query('your SQL here');

        return! ctx.WriteJsonAsync(users)
    }

Since our middleware uses an IDisposable for the connection, the connection will close on it's own on the scope termination of the http request.

Conclusion

In this post we've looked at the composable nature of a Giraffe API by writing a middleware that attaches one of two database connections to an http context. While a more object-oriented API would use other methods to attach functionality such as app-wide middleware, attributes, decorators, etc - we can accomplish this in F# and Giraffe with function composition.