@@ 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. 😁
@@ 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