Browse Source

[F#/Oxpecker] Changed db type from micro-ORM to raw + refactoring (#9118)

* [F#/Oxpecker] Switched from Dapper to raw ADO

(cherry picked from commit 2f386186a28f7dc502a3d3c7da35211e12d395f0)

* [F#/Oxpecker] Refactoring

* [F#/Oxpecker] Updated Readme

* [F#/Oxpecker] Updated Readme

* [F#/Oxpecker]  Fixed "versus" field
Vladimir Shchur 1 year ago
parent
commit
b9ced2e628

+ 4 - 7
frameworks/FSharp/oxpecker/README.md

@@ -1,5 +1,5 @@
 # Oxpecker Tests on Linux
 # Oxpecker Tests on Linux
-This includes tests for plaintext, json, and fortunes HTML serialization.
+This includes tests for plaintext, json, fortunes, single query, mutliple queries and data updates.
 
 
 ## Infrastructure Software Versions
 ## Infrastructure Software Versions
 
 
@@ -18,9 +18,6 @@ This includes tests for plaintext, json, and fortunes HTML serialization.
 **Web Stack**
 **Web Stack**
 
 
 * [Oxpecker](https://github.com/Lanayx/Oxpecker)
 * [Oxpecker](https://github.com/Lanayx/Oxpecker)
-* [Dapper](https://github.com/DapperLib/Dapper)
-* ASP.NET Core
-
-## Paths & Source for Tests
-
-All source code is inside `Program.fs`.
+* [Npgsql](https://github.com/npgsql/npgsql)
+* [System.Text.Json](https://github.com/dotnet/runtime/tree/main/src/libraries/System.Text.Json)
+* ASP.NET Core

+ 2 - 2
frameworks/FSharp/oxpecker/benchmark_config.json

@@ -15,7 +15,7 @@
         "database": "Postgres",
         "database": "Postgres",
         "framework": "Oxpecker",
         "framework": "Oxpecker",
         "language": "F#",
         "language": "F#",
-        "orm": "Micro",
+        "orm": "Raw",
         "platform": ".NET",
         "platform": ".NET",
         "flavor": "CoreCLR",
         "flavor": "CoreCLR",
         "webserver": "Kestrel",
         "webserver": "Kestrel",
@@ -23,7 +23,7 @@
         "database_os": "Linux",
         "database_os": "Linux",
         "display_name": "Oxpecker",
         "display_name": "Oxpecker",
         "notes": "",
         "notes": "",
-        "versus": "aspcore"
+        "versus": "aspnetcore"
       }
       }
     }
     }
   ]
   ]

+ 2 - 2
frameworks/FSharp/oxpecker/config.toml

@@ -13,7 +13,7 @@ classification = "fullstack"
 database = "Postgres"
 database = "Postgres"
 database_os = "Linux"
 database_os = "Linux"
 os = "Linux"
 os = "Linux"
-orm = "micro"
+orm = "Raw"
 platform = ".NET"
 platform = ".NET"
 webserver = "Kestrel"
 webserver = "Kestrel"
-versus = "aspcore"
+versus = "aspnetcore"

+ 2 - 1
frameworks/FSharp/oxpecker/src/App/App.fsproj

@@ -6,12 +6,13 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
+    <Compile Include="Common.fs" />
+    <Compile Include="Db.fs" />
     <Compile Include="Program.fs" />
     <Compile Include="Program.fs" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
     <PackageReference Update="FSharp.Core" Version="8.0.300" />
     <PackageReference Update="FSharp.Core" Version="8.0.300" />
-    <PackageReference Include="Dapper" Version="2.1.44" />
     <PackageReference Include="Oxpecker" Version="0.10.1" />
     <PackageReference Include="Oxpecker" Version="0.10.1" />
     <PackageReference Include="Npgsql" Version="8.0.3" />
     <PackageReference Include="Npgsql" Version="8.0.3" />
   </ItemGroup>
   </ItemGroup>

+ 34 - 0
frameworks/FSharp/oxpecker/src/App/Common.fs

@@ -0,0 +1,34 @@
+namespace App
+
+open System
+open System.Collections.Generic
+
+[<AutoOpen>]
+module Common =
+
+    [<Struct>]
+    [<CLIMutable>]
+    type JsonMessage = {
+        message : string
+    }
+
+    [<CLIMutable>]
+    type Fortune = {
+        id: int
+        message: string
+    }
+
+    [<Struct>]
+    [<CLIMutable>]
+    type World = {
+        id: int
+        randomnumber: int
+    }
+
+    [<Literal>]
+    let ConnectionString = "Server=tfb-database;Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;SSL Mode=Disable;Maximum Pool Size=1024;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4;Multiplexing=true;Write Coalescing Buffer Threshold Bytes=1000"
+
+    let FortuneComparer = {
+        new IComparer<Fortune> with
+            member self.Compare(a,b) = String.CompareOrdinal(a.message, b.message)
+    }

+ 104 - 0
frameworks/FSharp/oxpecker/src/App/Db.fs

@@ -0,0 +1,104 @@
+namespace App
+
+open System
+open System.Data
+open System.Data.Common
+open System.Text
+open Npgsql
+
+
+[<AutoOpen>]
+module Db =
+    let loadFortunes () =
+        let result = ResizeArray()
+        task {
+            use db = new NpgsqlConnection(ConnectionString)
+            use cmd = db.CreateCommand(CommandText = "SELECT id, message FROM fortune")
+            do! db.OpenAsync()
+            use! rdr = cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection)
+            while! rdr.ReadAsync() do
+                result.Add { id = rdr.GetInt32(0); message = rdr.GetString(1) }
+            return result
+        }
+
+    let private createReadCommand (connection: DbConnection) =
+        let cmd = connection.CreateCommand(
+            CommandText = "SELECT id, randomnumber FROM world WHERE id = @Id"
+        )
+        let id = cmd.CreateParameter(
+            ParameterName = "@Id",
+            DbType = DbType.Int32,
+            Value = Random.Shared.Next(1, 10001)
+        )
+        cmd.Parameters.Add(id) |> ignore
+        cmd
+
+    let private readSingleRow (cmd: DbCommand) =
+        task {
+            use! rdr = cmd.ExecuteReaderAsync(CommandBehavior.SingleRow)
+            let! _ = rdr.ReadAsync()
+            return { id = rdr.GetInt32(0); randomnumber = rdr.GetInt32(1) }
+        }
+
+    let loadSingleRow () =
+        task {
+            use db = new NpgsqlConnection(ConnectionString)
+            do! db.OpenAsync()
+            use cmd = createReadCommand db
+            return! readSingleRow cmd
+        }
+
+    let private readMultipleRows (count: int) (conn: NpgsqlConnection) =
+        let result = Array.zeroCreate count
+        task {
+            use cmd = createReadCommand conn
+            for i in 0..result.Length-1 do
+                cmd.Parameters["@Id"].Value <- Random.Shared.Next(1, 10001)
+                let! row = readSingleRow cmd
+                result[i] <- row
+            return result
+        }
+
+    let loadMultipleRows (count: int) =
+        task {
+            use db = new NpgsqlConnection(ConnectionString)
+            do! db.OpenAsync()
+            return! readMultipleRows count db
+        }
+
+    let private maxBatch = 500
+    let private queries = Array.zeroCreate (maxBatch + 1)
+    let private batchUpdateString batchSize =
+        match queries[batchSize] with
+        | null ->
+            let lastIndex = batchSize - 1
+            let sb = StringBuilder()
+            sb.Append("UPDATE world SET randomNumber = temp.randomNumber FROM (VALUES ") |> ignore
+            for i in 0..lastIndex-1 do
+                sb.AppendFormat("(@Id_{0}, @Rn_{0}), ", i) |> ignore
+            sb.AppendFormat("(@Id_{0}, @Rn_{0}) ORDER BY 1) AS temp(id, randomNumber) WHERE temp.id = world.id", lastIndex) |> ignore
+            let result = sb.ToString()
+            queries[batchSize] <- result
+            result
+        | q ->
+            q
+
+    let private generateParameters (results: World[]) (command: DbCommand) =
+        for i in 0..results.Length-1 do
+            let randomNumber = Random.Shared.Next(1, 10001)
+            let random = command.CreateParameter(ParameterName = $"@Rn_{i}", DbType = DbType.Int32, Value = randomNumber)
+            command.Parameters.Add(random) |> ignore
+            let id = command.CreateParameter(ParameterName = $"@Id_{i}", DbType = DbType.Int32, Value = results[i].id)
+            command.Parameters.Add(id) |> ignore
+            results[i] <- { results[i] with randomnumber = randomNumber }
+
+    let doMultipleUpdates (count: int) =
+        task {
+            use conn = new NpgsqlConnection(ConnectionString)
+            do! conn.OpenAsync()
+            let! results = readMultipleRows count conn
+            use cmd = conn.CreateCommand(CommandText = batchUpdateString count)
+            do generateParameters results cmd
+            let! _ = cmd.ExecuteNonQueryAsync()
+            return results
+        }

+ 14 - 95
frameworks/FSharp/oxpecker/src/App/Program.fs

@@ -1,32 +1,8 @@
 namespace App
 namespace App
 
 
 open System
 open System
-open System.Collections.Generic
 open Oxpecker
 open Oxpecker
 
 
-[<AutoOpen>]
-module Common =
-
-    [<Struct>]
-    [<CLIMutable>]
-    type JsonMessage = {
-        message : string
-    }
-
-    [<CLIMutable>]
-    type Fortune = {
-        id: int
-        message: string
-    }
-
-    [<Literal>]
-    let ConnectionString = "Server=tfb-database;Database=hello_world;User Id=benchmarkdbuser;Password=benchmarkdbpass;SSL Mode=Disable;Maximum Pool Size=1024;NoResetOnClose=true;Enlist=false;Max Auto Prepare=4;Multiplexing=true;Write Coalescing Buffer Threshold Bytes=1000"
-
-    let FortuneComparer = {
-        new IComparer<Fortune> with
-            member self.Compare(a,b) = String.CompareOrdinal(a.message, b.message)
-    }
-
 [<RequireQualifiedAccess>]
 [<RequireQualifiedAccess>]
 module HtmlViews =
 module HtmlViews =
     open Oxpecker.ViewEngine
     open Oxpecker.ViewEngine
@@ -48,7 +24,7 @@ module HtmlViews =
             th() { raw "message" }
             th() { raw "message" }
         }
         }
 
 
-    let fortunes fortunesData =
+    let fortunes (fortunesData: ResizeArray<Fortune>) =
         table() {
         table() {
             fortunesTableHeader
             fortunesTableHeader
             for fortune in fortunesData do
             for fortune in fortunesData do
@@ -60,8 +36,6 @@ module HtmlViews =
 
 
 [<RequireQualifiedAccess>]
 [<RequireQualifiedAccess>]
 module HttpHandlers =
 module HttpHandlers =
-    open Dapper
-    open Npgsql
     open System.Text
     open System.Text
     open Microsoft.AspNetCore.Http
     open Microsoft.AspNetCore.Http
     open System.Text.Json
     open System.Text.Json
@@ -72,32 +46,24 @@ module HttpHandlers =
             message = "Additional fortune added at request time."
             message = "Additional fortune added at request time."
         }
         }
 
 
-    let rec private renderFortunes (ctx: HttpContext) (dbFortunes: Fortune seq) =
-        let data = dbFortunes.AsList()
+    let rec private renderFortunes (ctx: HttpContext) (data: ResizeArray<Fortune>) =
         data.Add extra
         data.Add extra
         data.Sort FortuneComparer
         data.Sort FortuneComparer
         data |> HtmlViews.fortunes |> ctx.WriteHtmlView
         data |> HtmlViews.fortunes |> ctx.WriteHtmlView
 
 
-    let private fortunes : EndpointHandler =
+    let fortunes : EndpointHandler =
         fun ctx ->
         fun ctx ->
             task {
             task {
-                use conn = new NpgsqlConnection(ConnectionString)
-                let! dbFortunes = conn.QueryAsync<Fortune>("SELECT id, message FROM fortune")
+                let! dbFortunes = loadFortunes ()
                 return! renderFortunes ctx dbFortunes
                 return! renderFortunes ctx dbFortunes
             }
             }
 
 
-    [<Struct>]
-    [<CLIMutable>]
-    type World = {
-        id: int
-        randomnumber: int
-    }
-
-    let private readSingleRow (conn: NpgsqlConnection) =
-        conn.QueryFirstOrDefaultAsync<World>(
-            "SELECT id, randomnumber FROM world WHERE id = @Id",
-            {| Id = Random.Shared.Next(1, 10001) |}
-        )
+    let singleQuery : EndpointHandler =
+        fun ctx ->
+            task {
+                let! result = loadSingleRow()
+                return! ctx.WriteJsonChunked result
+            }
 
 
     let private parseQueries (ctx: HttpContext) =
     let private parseQueries (ctx: HttpContext) =
         match ctx.TryGetRouteValue<string>("count") with
         match ctx.TryGetRouteValue<string>("count") with
@@ -107,66 +73,19 @@ module HttpHandlers =
             | _, _ -> 1
             | _, _ -> 1
         | _ -> 1
         | _ -> 1
 
 
-    let private singleQuery : EndpointHandler =
-        fun ctx ->
-            task {
-                use conn = new NpgsqlConnection(ConnectionString)
-                let! result = readSingleRow conn
-                return! ctx.WriteJsonChunked result
-            }
-
-    let private multipleQueries : EndpointHandler =
+    let multipleQueries : EndpointHandler =
         fun ctx ->
         fun ctx ->
             let count = parseQueries ctx
             let count = parseQueries ctx
-            let results = Array.zeroCreate<World> count
             task {
             task {
-                use conn = new NpgsqlConnection(ConnectionString)
-                do! conn.OpenAsync()
-                for i in 0..results.Length-1 do
-                    let! result = readSingleRow conn
-                    results[i] <- result
+                let! results = loadMultipleRows count
                 return! ctx.WriteJsonChunked results
                 return! ctx.WriteJsonChunked results
             }
             }
 
 
-    let private maxBatch = 500
-    let private queries = Array.zeroCreate (maxBatch + 1)
-
-    let private batchUpdateString batchSize =
-        match queries[batchSize] with
-        | null ->
-            let lastIndex = batchSize - 1
-            let sb = StringBuilder()
-            sb.Append("UPDATE world SET randomNumber = temp.randomNumber FROM (VALUES ") |> ignore
-            for i in 0..lastIndex-1 do
-                sb.AppendFormat("(@Id_{0}, @Rn_{0}), ", i) |> ignore
-            sb.AppendFormat("(@Id_{0}, @Rn_{0}) ORDER BY 1) AS temp(id, randomNumber) WHERE temp.id = world.id", lastIndex) |> ignore
-            let result = sb.ToString()
-            queries[batchSize] <- result
-            result
-        | q ->
-            q
-
-    let private generateParameters (results: World[]) =
-        let parameters = Dictionary<string,obj>()
-        for i in 0..results.Length-1 do
-            let randomNumber = Random.Shared.Next(1, 10001)
-            parameters[$"@Rn_{i}"] <- randomNumber
-            parameters[$"@Id_{i}"] <- results[i].id
-            results[i] <- { results[i] with randomnumber = randomNumber }
-        parameters
-
-    let private multipleUpdates : EndpointHandler =
+    let multipleUpdates : EndpointHandler =
         fun ctx ->
         fun ctx ->
             let count = parseQueries ctx
             let count = parseQueries ctx
-            let results = Array.zeroCreate<World> count
             task {
             task {
-                use conn = new NpgsqlConnection(ConnectionString)
-                do! conn.OpenAsync()
-                for i in 0..results.Length-1 do
-                    let! result = readSingleRow conn
-                    results[i] <- result
-                let parameters = generateParameters results
-                let! _ = conn.ExecuteAsync(batchUpdateString count, parameters)
+                let! results = doMultipleUpdates count
                 return! ctx.WriteJsonChunked results
                 return! ctx.WriteJsonChunked results
             }
             }