~glorifiedgluer/gluer.org

1b8142c38ba2b68ac958158764a8f9cad0152681 — Victor Freire 5 months ago 5487971
article: having-fun-with-f-sql-databases-and-asp-dot-net
M content-org/content.org => content-org/content.org +251 -0
@@ 4783,8 4783,259 @@ with
    return  Error(Unknown ex)
#+end_src

** Having fun with F#, SQL Databases and ASP.NET             :fsharp:dotnet:
:PROPERTIES:
:EXPORT_DATE: 2023-06-14
:EXPORT_FILE_NAME: having-fun-with-f-sql-databases-and-asp-dot-net
:EXPORT_HUGO_SLUG: having-fun-with-f-sql-databases-and-asp-dot-net
:END:

#+attr_shortcode: :class info
#+begin_alert
You can read the [[file:/blog/working-with-a-database-using-sqlfun-and-asp-dot-net/script.fsx][source code]] for this article.
#+end_alert

#+begin_description
Designing a data access layer with SqlFun for an ASP.NET application.
#+end_description

For some time now, I've been waiting for an opportunity to use [[https://github.com/jacentino/SqlFun][SqlFun]][fn:43],
a data access library for F#. Yesterday, it finally happened when a
friend of mine decided to give F# a go.

What's the deal with this library? You can watch the talk "[[https://www.youtube.com/watch?v=ourkF5bM8Sc][F#, SQL and
lot of fun F# SQL micro orm]]" to understand some of its design
decisions. However, summarizing what it does in practice: it provides
a /quasi/ static analysis of your SQL queries. This is a vague
statement, yes, but it means that the library will check, *before* you
run any query, if you object is correctly mapping with your query
result, if the query has correct parameters and so on.

While this /might/ be less powerful than something as
[[https://github.com/demetrixbio/FSharp.Data.Npgsql][FSharp.Data.Npgsql]] that provides actual compile-time guarantees for
PostgreSQL databases. It has the advantage of keeping your build time
dependencies less complex, as you don't need to deal with DLLs or have
a running database during build-time.

Previously, the main problem I had with the library was writing code
that didn't depend on /module state/ such as the project's README.
While I, personally, don't like this approach, it does have an
advantage that I was not able to fully reproduce on my approach. When
you hold the state on your module, the only thing you need to worry
about is to actually use the module once. In SqlFun's case, this would
mean that a simple ~open~ statement would validate the queries.

Let's deep dive into the code. In order to make things easier to deal
with in a F# script, the database of choice will be [[https://www.sqlite.org/index.html][SQLite]]. Install
the needed packages and open them:

#+begin_src fsharp
#r "nuget: SqlFun, 2.2.1"
#r "nuget: Microsoft.Data.Sqlite, 8.0.0-preview.5.23280.1"

open SqlFun
open SqlFun.GeneratorConfig
open SqlFun.Queries
open Microsoft.Data.Sqlite
#+end_src

This is all we need to have access to SQLite and SqlFun on our script.
SqlFun needs some boilerplate code to make our lives easier:

#+begin_src fsharp
module Database =
  // string -> unit -> SqliteConnection
  let createConnection connectionStr: unit -> SqliteConnection =
      (fun () -> new SqliteConnection(connectionStr))
  // (unit -> 'a (requires :> IDbConnection)) -> GeneratorConfig
  let generatorConfig connection = createDefaultConfig connection
  // (unit -> 'a) -> string -> 'b
  let sql connection commandText = sql (generatorConfig connection) commandText
  // string -> (IDataContext -> 'a) -> 'a
  let run connectionStr f =
      DbAction.run (createConnection connectionStr) f
#+end_src

This is trivial, we receive a connection string, open the database,
create the tables and close the database. Now comes the interesting
part! SqlFun needs some boilerplate code to make our lives easier:

Breaking this code apart, we have four functions:

- ~createConnection~: this function receives a connection string and
  returns another function that, when called, returns a SQLite
  connection.
- ~generatorConfig~: looking at the function signature, the
  ~connection~ parameter expects the exact result of
  ~createConnection~ when you pass just the connection string. It then
  returns a configuration record.
- ~sql~: here, again, we receive a connection string and a SQL query string.
- ~run~: different from the other function, ~run~ expects an opened
  connection and a function that we will define later in our
  /repositories/.

Now, let's write a small piece of code to create our database tables:

#+begin_src fsharp
module Infrastructure =
  let createDatabase (connectionStr) =
    let db = new SqliteConnection(connectionStr)
    db.Open()

    let cmd = new SqliteCommand("
      CREATE TABLE user (
        ID INTEGER NOT NULL,
        NAME TEXT NOT NULL
      )
    ", db)
    cmd.ExecuteNonQueryAsync() |> ignore
    db.Close() |> ignore
#+end_src

Define a domain record to be used on our repository:

#+begin_src fsharp
module Domain =
  type User = { Id: string; Name: string }
#+end_src

Here comes the time to define our ~Repository~ module:

#+begin_src fsharp
module Repository =
  open Database

  type User(connection) =
      member val Get: int -> DbAction<Domain.User> =
        sql connection "select id, name from user where id = @id"

  type Repository =
    { User: User }

  let create connectionStr =
    let db = createConnection connectionStr ()
    let connection = (fun _ -> db)

    { User = User(connectionStr) }
#+end_src

The ~Repository~ module contains a ~User~ class that receives a ~unit
-> IDbConnection~ as a constructor parameter. This connection is then
passed to the ~sql~ function inside each member of the class. Note
that the ~Get~ member is a ~val~, not a method. This is important
because it allows us to evaluate the ~sql~ function during the class
instantiation.

The ~sql~ connects to the database and generates the code needed to
process the query. However, note that this code generation is stored
on the class member. Everything is ready for you to start calling your
query through your repository methods.

As previously mentioned, the disadvantage of the class approach
compared to the module one is that you have to keep track of your
instance throughout your application. Thankfully, it's possible to
borrow something I learned on Alex Edwards' [[https://lets-go.alexedwards.net/][Let's Go]] book. In Go you
would store your repositories on a single ~struct~ that would be
passed around your application as a dependency:

#+begin_src go
type Models struct {
  User UserRepository
}

func NewModels(db *sql.DB) {
    return Models{
        User: NewUserRepository(db)
    }
}
#+end_src

It's easy to do the same in F#, but using records instead of structs.
This is exactly what the ~Repository~ record represents!

Putting this code to use looks like the following:

#+begin_src fsharp
let connectionStr = "Data Source=sqlite.db"

Infrastructure.createDatabase connectionStr
let repository = Repository.create connectionStr
#+end_src

The funny part is that running this code actually showed an error on
my program:

#+begin_example
SqlFun.Exceptions.CompileTimeException: Error generating function Int32 -> IDataContext -> User for sql command select id, name from user where id = @id
 ---> System.Exception: No column found for Id field. Expected: Id
 ---> System.InvalidOperationException: No coercion operator is defined between types 'System.Int64' and 'System.String'.
#+end_example

Our domain is incorrect, ~User~ has an ~int~ Id, not a string Id! The
correct implementation is this:

#+begin_src fsharp
module Domain =
  type User = { Id: int; Name: string }
#+end_src

*** We are almost there

One thing to remember is that SqlFun needs to actually run a query to
determine if the return type should be an ~option~ or not.

#+begin_src fsharp
let repository = Repository.create connectionStr
repository.User.Get 123 |> Database.run connection
#+end_src

#+begin_src example
System.Exception: Value does not exist. Use option type.
#+end_src

*** Plugging our repository to ASP.NET

So far this library seems to be groovy but how could we proceed to
plug it on an ASP.NET application? This can be done through ASP.NET's
own [[https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-7.0][Dependency Injection]] mechanism.

#+begin_src fsharp
let configureServices config (services: IServiceCollection) =
    let repository = Repository.create config.SqlConnectionString

    services.TryAddSingleton<Repository.Repository>
        (fun _ -> repository) |> ignore

    services
#+end_src

The repository is instantiated allowing SqlFun to validate the queries
contained on the inner repositories as previously explained. After
this, adding a [[https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#singleton][Singleton]] service one for the repository. Why
singleton? Because using the same instance throughout the lifetime of
our web application avoids validating the queries again after startup.

A [[https://github.com/giraffe-fsharp/Giraffe][Giraffe]] handler would look like this:

#+begin_src fsharp
let getUser config userId : HttpHandler =
  fun (next: HttpFunc) (ctx: HttpContext) -> task {
    let repository = ctx.GetService<Repository.Repository>()

    let user =
      repository.User.Get userId
      |> Database.run config.SqlConnectionString

      return!
        match user with
        | Some _ -> text "user found" next ctx
        | None -> RequestErrors.notFound (text "user not found") next ctx
  }
#+end_src

* Footnotes
[fn:43] Actually, there was a [[https://github.com/jacentino/SqlFun/issues/16][previous attempt]], but I failed
miserably. This article is my /redemption arc/ with the library. 🥲

[fn:42] Which just expects the sum of the areas. 😁


A static/blog/working-with-a-database-using-sqlfun-and-asp-dot-net/script.fsx => static/blog/working-with-a-database-using-sqlfun-and-asp-dot-net/script.fsx +52 -0
@@ 0,0 1,52 @@
#r "nuget: SqlFun, 2.2.1"
#r "nuget: Microsoft.Data.Sqlite, 8.0.0-preview.5.23280.1"

open Microsoft.Data.Sqlite
open SqlFun
open SqlFun.GeneratorConfig
open SqlFun.Queries

module Database =
  let createConnection connectionStr: unit -> SqliteConnection = fun () -> new SqliteConnection(connectionStr)
  let generatorConfig connection = createDefaultConfig connection
  let sql connection commandText = sql (generatorConfig connection) commandText
  let run connectionStr f = DbAction.run (createConnection connectionStr) f

module Infrastructure =
  let createDatabase (connectionStr) =
    let db = Database.createConnection connectionStr ()
    db.Open()

    let cmd = new SqliteCommand("
      CREATE TABLE user (
        ID INTEGER NOT NULL,
        NAME TEXT NOT NULL
      )
    ", db)
    cmd.ExecuteNonQueryAsync() |> ignore
    db.Close() |> ignore

module Domain =
  type User = { Id: int; Name: string }

module Repository =
  open Database

  type User(connection) =
      member val Get: int -> DbAction<Domain.User option> =
        sql connection "select id, name from user where id = @id"

  type Repository =
    { User: User; }

  let create (connectionStr) =
    let db = createConnection connectionStr ()
    let connection = (fun _ -> db)

    { User = User(connection) }

let connectionStr = "Data Source=sqlite.db"

Infrastructure.createDatabase connectionStr
let repo = Repository.create connectionStr
repo.User.Get 123 |> Database.run connectionStr