Saturn is a modern MVC-oriented web framework for F# built on .NET Core, making it a suitable option for cross-platform development and deployment on unix-like operating systems, which for me is a must. The F# community is really enthusiastic about a feature of the F# language and environment which they lovingly refer to as type providers. When it comes to type providers, the objective is usually to provide some kind of immediate and type-safe mapping between an external data source and your F# codebase. This is immensely convenient since in F#, as with any functional language, types are a major source of developer power. Being able to generate types from structured external data for use within your F# code removes a lot of the hassle and worry that might otherwise accompany any effort to access external data.

However, being a .NET language, for a long while much of the documentation and guidance online for F# was oriented toward Windows and Visual Studio users. Although they were appealing, I had a difficult time trying to make use of the SQL providers on Linux, and for a long while this turned me away from attempting any web development in F#. Although I’m sure the situation has improved on that front, I decided to see how easy it would be to use PostgreSQL on Linux with Saturn compared to my attempt to use it with Suave in 2017. It didn’t take very long, but for future reference (for myself) I’ve decided to lay out the process here.

Setting up a Saturn Project

Saturn provides really convenient tooling for setting up your project. If you already have .NET Core 3.1 installed, you can execute the following in a directory of your choice to get a working project put together for you:

dotnet new -i Saturn.Template
mkdir SaturnSample && cd SaturnSample
dotnet new saturn -lang F#
dotnet tool restore
dotnet saturn gen Book Books id:string title:string author:string

Open the file …/SaturnSample/src/SaturnSample/Router.fs and add the specified line:

// This is the part of the file you're editing
let browserRouter = router {
  not_found_handler (htmlView NotFound.layout)
  pipe_through browser

  forward "" defaultView
  // This is the line you're adding
  forward "/books" Books.Controller.resource
}

These instructions are taken directly from the How to Start guide provided in the Saturn documentation, and if you’d like further information about what they do you should check them out there. I excluded the migration and build steps, but that’s because I want to change the project to use PostgreSQL before we migrate and build. Know, however, that if you’ve already migrated and built your project for SQLite, the rest of this guide can still be followed exactly without any problems. Simple.Migrations will run the migration again for PostgreSQL once you make the necessary changes.

Setting up a PostgreSQL Database with Docker

Although it’s not at all necessary that you use Docker for this, I prefer containerising my databases especially for scrap projects like this. If you’d like to set up a local bare-metal installation of PostgreSQL, you are free to do so.

With Docker installed and the Docker service enabled, run the following:

docker run --name your-container-name -e POSTGRES_PASSWORD=your_password -p 5432:5432 -d postgres

This will create a -detached container named your-container-name with an environment variable POSTGRES_PASSWORD set to your_password, mapping your local port 5432 to port 5432 in the container, and finally it will run postgres within the container. If you want to be sure that it works, you can try connecting to it locally (if you have PostgreSQL and subsequently psql installed):

psql -h localhost -p 5432 postgres -U postgres -W # You will be prompted for a password

You should be met with the standard PostgreSQL prompt, connected to the database postgres, which you can quit with \q.

Adjusting Your Saturn Project to Use Npgsql

Before we adjust the code that sets up the database connection, we should add Npgsql as a dependency in our project. The files we need to modify are the following:

…/SaturnSample/paket.dependencies
…/SaturnSample/paket.lock
…/SaturnSample/src/Migrations/paket.references
…/SaturnSample/src/SaturnSample/paket.references

To learn more about the different Paket files, what their unique purposes are and how they work together, you can read about it here.

In …/SaturnSample/paket.dependencies, find the line where Microsoft.Data.Sqlite is listed (the entire line should read nuget Microsoft.Data.Sqlite) and right above or below it, add the following line:

nuget Npgsql

In …/SaturnSample/paket.lock, beneath the line that reads remote: https://api.nuget.org/v3/index.json, include the following line with the version of Npgsql you want to use. I’m using the latest as of September 12th 2020:

Npgsql (4.1.4)

In …/SaturnSample/src/Migrations/paket.references and …/SaturnSample/src/SaturnSample/paket.references, simply add a line:

Npgsql

You can include it above or beneath Microsoft.Data.Sqlite.

In the root directory of your project, run dotnet restore to install Npgsql.

Adjusting the Migrations Code

Thankfully for us, we don’t need to change much at all to make our project, by default configured for SQLite, fully compatible with a PostgreSQL database.

Open up …/SaturnSample/src/Migrations/Program.fs. You should see the following:

module Program

open System.Reflection
open SimpleMigrations
open Microsoft.Data.Sqlite
open SimpleMigrations.DatabaseProvider
open SimpleMigrations.Console


[<EntryPoint>]
let main argv =
    let assembly = Assembly.GetExecutingAssembly()
    use db = new SqliteConnection "DataSource=src/SaturnSample/database.sqlite"
    let provider = SqliteDatabaseProvider(db)
    let migrator = SimpleMigrator(assembly, provider)
    let consoleRunner = ConsoleRunner(migrator)
    consoleRunner.Run(argv) |> ignore
    0

The only SQLite specific parts of this code are the following lines:

use db = new SqliteConnection "DataSource=src/SaturnSample/database.sqlite"
let provider = SqliteDatabaseProvider(db)

Changing this is simple. Make sure you open Npgsql underneath or below open Microsoft.Data.Sqlite. Once you’ve done that, go ahead and make the following changes:

// Change use db = new SqliteConnection "..." to
use db = new NpgsqlConnection "Host=localhost;Username=postgres;Password=your_password;Database=postgres"
// Change let provider = SqliteDatabaseProvider(db) to
let provider = PostgresqlDatabaseProvider(db)

One last change before we migrate and build our project: in …/SaturnSample/src/SaturnSample/Program.fs, change the following line:

// Change use_config (fun _ -> {connectionString = "DataSource=src/SaturnSample/database.sqlite"} ) to
use_config (fun _ -> {connectionString = "Host=localhost;Username=postgres;Password=your_password;Database=postgres"} )

This is not exactly necessary, as development time configuration has not fully been implemented to my knowledge, but if it will be implemented in the near future, it’s not a bad idea to have be prepared to take advantage of it.

Simple.Migrations only needs an initialised DatabaseProvider to work with, so all we had to do was make sure we created and passed a PostgresqlDatabaseProvider instead of a SqliteDatabaseProvider, which we can do easily with Npgsql.

Migrating and Building

This is the easiest step. The following commands should successfully create a table for the Book model we generated earlier, and then build and run your application:

dotnet saturn migration
dotnet fake build -t run

Once the project finishes building and running, you should be able to interact with the part you added at https://localhost:8085/books.

Enjoy developing full-stack F# web applications on Linux with Saturn!