From df39f7b39fb46ef44c9726df8824cdfe182d55b9 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Tue, 21 Aug 2018 20:03:23 +0100 Subject: [PATCH 01/43] Updated PS scripts Minor improvements. --- .psscripts/build-functions.ps1 | 59 +++++++++++++++++++ .../{nuget.ps1 => check-nuget-updates.ps1} | 0 .psscripts/install-dotnet.ps1 | 56 +----------------- build.ps1 | 2 +- 4 files changed, 62 insertions(+), 55 deletions(-) rename .psscripts/{nuget.ps1 => check-nuget-updates.ps1} (100%) diff --git a/.psscripts/build-functions.ps1 b/.psscripts/build-functions.ps1 index 59e2dfed..8207c43d 100644 --- a/.psscripts/build-functions.ps1 +++ b/.psscripts/build-functions.ps1 @@ -167,6 +167,65 @@ function Write-DotnetCoreVersions Write-Host ".NET Core Runtime version: $runtimeVersion" -ForegroundColor Cyan } +function Get-DesiredSdk +{ + <# + .DESCRIPTION + Gets the desired .NET Core SDK version from the global.json file. + #> + + Get-Content "global.json" ` + | ConvertFrom-Json ` + | ForEach-Object { $_.sdk.version.ToString() } +} + +function Download-NetCoreSdk ($version) +{ + <# + .DESCRIPTION + Downloads the desired .NET Core SDK version from the internet and saves it under a temporary file name which will be returned by the function. + + .PARAMETER version + The SDK version which should be downloaded. + #> + + $os = if (Test-IsWindows) { "windows" } else { "linux" } + + $response = Invoke-WebRequest ` + -Uri "https://www.microsoft.com/net/download/thank-you/dotnet-sdk-$version-$os-x64-binaries" ` + -Method Get ` + -MaximumRedirection 0 ` + + $downloadLink = + $response.Links ` + | Where-Object { $_.onclick -eq "recordManualDownload()" } ` + | Select-Object -Expand href + + $tempFile = [System.IO.Path]::GetTempFileName() + $webClient = New-Object System.Net.WebClient + $webClient.DownloadFile($downloadLink, $tempFile) + return $tempFile +} + +function Install-NetCoreSdk ($sdkZipPath) +{ + <# + .DESCRIPTION + Extracts the zip archive which contains the .NET Core SDK and installs it in the current working directory under .dotnetsdk. + + .PARAMETER version + The zip archive which contains the .NET Core SDK. + #> + + + $env:DOTNET_INSTALL_DIR = "$pwd\.dotnetsdk" + New-Item $env:DOTNET_INSTALL_DIR -ItemType Directory -Force + + Add-Type -AssemblyName System.IO.Compression.FileSystem; + [System.IO.Compression.ZipFile]::ExtractToDirectory($sdkZipPath, $env:DOTNET_INSTALL_DIR) + $env:Path = "$env:DOTNET_INSTALL_DIR;$env:Path" +} + # ---------------------------------------------- # AppVeyor functions # ---------------------------------------------- diff --git a/.psscripts/nuget.ps1 b/.psscripts/check-nuget-updates.ps1 similarity index 100% rename from .psscripts/nuget.ps1 rename to .psscripts/check-nuget-updates.ps1 diff --git a/.psscripts/install-dotnet.ps1 b/.psscripts/install-dotnet.ps1 index 7f3770e2..f52a9e6a 100644 --- a/.psscripts/install-dotnet.ps1 +++ b/.psscripts/install-dotnet.ps1 @@ -1,63 +1,11 @@ -# ---------------------------------------------------------- -# Install script to check and download the correct .NET SDK -# ---------------------------------------------------------- - -function Test-IsWindows -{ - [environment]::OSVersion.Platform -ne "Unix" -} - -function Invoke-Cmd ($cmd) -{ - Write-Host $cmd -ForegroundColor DarkCyan - if (Test-IsWindows) { $cmd = "cmd.exe /C $cmd" } - Invoke-Expression -Command $cmd - if ($LastExitCode -ne 0) { Write-Error "An error occured when executing '$cmd'."; return } -} - -function dotnet-version { Invoke-Cmd "dotnet --version" } - -function Get-DesiredSdk -{ - Get-Content "global.json" | ConvertFrom-Json | % { $_.sdk.version.ToString() } -} - -function Get-NetCoreSdk ($version) -{ - $os = if (Test-IsWindows) { "windows" } else { "linux" } - - $response = Invoke-WebRequest ` - -Uri "https://www.microsoft.com/net/download/thank-you/dotnet-sdk-$version-$os-x64-binaries" ` - -Method Get ` - -MaximumRedirection 0 ` - - $downloadLink = - $response.Links ` - | Where-Object { $_.onclick -eq "recordManualDownload()" } ` - | Select-Object -Expand href - - $tempFile = [System.IO.Path]::GetTempFileName() - $webClient = New-Object System.Net.WebClient - $webClient.DownloadFile($downloadLink, $tempFile) - return $tempFile -} - -function Install-NetCoreSdk ($sdkZipPath) -{ - $env:DOTNET_INSTALL_DIR = "$pwd\.dotnetsdk" - New-Item $env:DOTNET_INSTALL_DIR -ItemType Directory -Force - - Add-Type -AssemblyName System.IO.Compression.FileSystem; - [System.IO.Compression.ZipFile]::ExtractToDirectory($sdkZipPath, $env:DOTNET_INSTALL_DIR) - $env:Path = "$env:DOTNET_INSTALL_DIR;$env:Path" -} - # ---------------------------------------------- # Install .NET Core SDK # ---------------------------------------------- $ErrorActionPreference = "Stop" +Import-module "$PSScriptRoot\build-functions.ps1" -Force + # Rename the global.json before making the dotnet --version call # This will prevent AppVeyor to fail because it might not find # the desired SDK specified in the global.json diff --git a/build.ps1 b/build.ps1 index e25fd409..5dc74346 100644 --- a/build.ps1 +++ b/build.ps1 @@ -24,7 +24,7 @@ Write-Host " Starting Giraffe build script " -ForegroundColor DarkYellow Write-Host "--------------------------------" -ForegroundColor DarkYellow Write-Host "" -Import-module "$pwd\.psscripts\build-functions.ps1" -Force +Import-module "$PSScriptRoot\.psscripts\build-functions.ps1" -Force if ($ClearOnly.IsPresent) { From 648fa3174b0bf7cacf44e4d2a3fac38c3aa7ac1a Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Tue, 21 Aug 2018 20:06:37 +0100 Subject: [PATCH 02/43] Fixed wrong name of function Mini fix. --- .psscripts/build-functions.ps1 | 2 +- .psscripts/install-dotnet.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.psscripts/build-functions.ps1 b/.psscripts/build-functions.ps1 index 8207c43d..6b790d7b 100644 --- a/.psscripts/build-functions.ps1 +++ b/.psscripts/build-functions.ps1 @@ -179,7 +179,7 @@ function Get-DesiredSdk | ForEach-Object { $_.sdk.version.ToString() } } -function Download-NetCoreSdk ($version) +function Get-NetCoreSdkFromWeb ($version) { <# .DESCRIPTION diff --git a/.psscripts/install-dotnet.ps1 b/.psscripts/install-dotnet.ps1 index f52a9e6a..45a076b1 100644 --- a/.psscripts/install-dotnet.ps1 +++ b/.psscripts/install-dotnet.ps1 @@ -29,7 +29,7 @@ if ($desiredSdk -eq $currentSdk) Write-Host "The current .NET SDK ($currentSdk) doesn't match the project's desired .NET SDK ($desiredSdk)." -ForegroundColor Yellow Write-Host "Attempting to download and install the correct .NET SDK..." -$sdkZipPath = Get-NetCoreSdk $desiredSdk +$sdkZipPath = Get-NetCoreSdkFromWeb $desiredSdk Install-NetCoreSdk $sdkZipPath Write-Host ".NET SDK installation complete." -ForegroundColor Green From 1903231672fc39f009a159a94a5bcfe3ad607207 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Tue, 21 Aug 2018 21:47:45 +0100 Subject: [PATCH 03/43] Upgraded to .NET Core 2.1.401 Also removed the Optimize property from Giraffe, as it doesn't seem to add a lot of benefits and makes debugging more difficult. --- .psscripts/install-dotnet.ps1 | 2 +- .psscripts/{check-nuget-updates.ps1 => nuget-updates.ps1} | 2 +- .travis.yml | 2 +- global.json | 2 +- samples/SampleApp/SampleApp.Tests/Program.fs | 2 -- samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj | 3 +-- src/Giraffe/Giraffe.fsproj | 1 - tests/Giraffe.Tests/Giraffe.Tests.fsproj | 2 +- 8 files changed, 6 insertions(+), 10 deletions(-) rename .psscripts/{check-nuget-updates.ps1 => nuget-updates.ps1} (96%) delete mode 100644 samples/SampleApp/SampleApp.Tests/Program.fs diff --git a/.psscripts/install-dotnet.ps1 b/.psscripts/install-dotnet.ps1 index 45a076b1..1c71f646 100644 --- a/.psscripts/install-dotnet.ps1 +++ b/.psscripts/install-dotnet.ps1 @@ -9,7 +9,7 @@ Import-module "$PSScriptRoot\build-functions.ps1" -Force # Rename the global.json before making the dotnet --version call # This will prevent AppVeyor to fail because it might not find # the desired SDK specified in the global.json -$globalJson = Get-Item "global.json" +$globalJson = Get-Item "$PSScriptRoot\..\global.json" Rename-Item -Path $globalJson.FullName -NewName "global.json.bak" -Force # Get the current .NET Core SDK version diff --git a/.psscripts/check-nuget-updates.ps1 b/.psscripts/nuget-updates.ps1 similarity index 96% rename from .psscripts/check-nuget-updates.ps1 rename to .psscripts/nuget-updates.ps1 index 9a276218..f49c327b 100644 --- a/.psscripts/check-nuget-updates.ps1 +++ b/.psscripts/nuget-updates.ps1 @@ -36,7 +36,7 @@ Write-Host " Scanning all projects for NuGet package upgrades " Write-Host "--------------------------------------------------" Write-Host "" -$projects = Get-ChildItem "*.*proj" -Recurse | % { $_.FullName } +$projects = Get-ChildItem "..\**\*.*proj" -Recurse | % { $_.FullName } foreach ($project in $projects) { diff --git a/.travis.yml b/.travis.yml index 852d84b7..38b7417e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: csharp sudo: required dist: trusty -dotnet: 2.1.400 +dotnet: 2.1.401 mono: - 4.6.1 - 4.8.1 diff --git a/global.json b/global.json index b11d921c..e27592b0 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "projects": [ "src", "tests" ], "sdk": { - "version": "2.1.400" + "version": "2.1.401" } } \ No newline at end of file diff --git a/samples/SampleApp/SampleApp.Tests/Program.fs b/samples/SampleApp/SampleApp.Tests/Program.fs deleted file mode 100644 index 75f178df..00000000 --- a/samples/SampleApp/SampleApp.Tests/Program.fs +++ /dev/null @@ -1,2 +0,0 @@ -module Program -let [] main _ = 0 \ No newline at end of file diff --git a/samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj b/samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj index c8251c6b..0b66c7a6 100644 --- a/samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj +++ b/samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj @@ -9,7 +9,7 @@ - + @@ -20,7 +20,6 @@ - \ No newline at end of file diff --git a/src/Giraffe/Giraffe.fsproj b/src/Giraffe/Giraffe.fsproj index 9eb0a7a9..919d2ce3 100644 --- a/src/Giraffe/Giraffe.fsproj +++ b/src/Giraffe/Giraffe.fsproj @@ -13,7 +13,6 @@ net461;netstandard2.0 portable Library - true true false true diff --git a/tests/Giraffe.Tests/Giraffe.Tests.fsproj b/tests/Giraffe.Tests/Giraffe.Tests.fsproj index 040a7e5d..43c02c65 100644 --- a/tests/Giraffe.Tests/Giraffe.Tests.fsproj +++ b/tests/Giraffe.Tests/Giraffe.Tests.fsproj @@ -3,7 +3,7 @@ net461;netcoreapp2.1 Giraffe.Tests - false + portable From 5c4727a706ba521d9fcd813147309671a27f0e97 Mon Sep 17 00:00:00 2001 From: Quintus Marais Date: Fri, 24 Aug 2018 16:25:02 +0200 Subject: [PATCH 04/43] Fix typo in docs - bindFrom should be bindForm --- DOCUMENTATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index f82e436e..62b78f12 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -1318,7 +1318,7 @@ let webApp = ] ``` -Alternatively you can use the `bindFrom<'T>` http handler (which also accepts an additional parameter of type `CultureInfo option`): +Alternatively you can use the `bindForm<'T>` http handler (which also accepts an additional parameter of type `CultureInfo option`): ```fsharp [] From 5230c342b3125a3c113345cbd6ca5fd06ffb3a78 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Fri, 24 Aug 2018 23:35:44 +0100 Subject: [PATCH 05/43] Changed from backward to forward slashes in build script Changed the slashes in the build script because PowerShell doesn't care and it makes it easier to re-run only a single command on OSX by copy pasting. --- build.ps1 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/build.ps1 b/build.ps1 index 5dc74346..f2558f73 100644 --- a/build.ps1 +++ b/build.ps1 @@ -24,7 +24,7 @@ Write-Host " Starting Giraffe build script " -ForegroundColor DarkYellow Write-Host "--------------------------------" -ForegroundColor DarkYellow Write-Host "" -Import-module "$PSScriptRoot\.psscripts\build-functions.ps1" -Force +Import-module "$PSScriptRoot/.psscripts/build-functions.ps1" -Force if ($ClearOnly.IsPresent) { @@ -32,12 +32,12 @@ if ($ClearOnly.IsPresent) return } -$giraffe = ".\src\Giraffe\Giraffe.fsproj" -$giraffeTests = ".\tests\Giraffe.Tests\Giraffe.Tests.fsproj" -$identityApp = ".\samples\IdentityApp\IdentityApp\IdentityApp.fsproj" -$jwtApp = ".\samples\JwtApp\JwtApp\JwtApp.fsproj" -$sampleApp = ".\samples\SampleApp\SampleApp\SampleApp.fsproj" -$sampleAppTests = ".\samples\SampleApp\SampleApp.Tests\SampleApp.Tests.fsproj" +$giraffe = "./src/Giraffe/Giraffe.fsproj" +$giraffeTests = "./tests/Giraffe.Tests/Giraffe.Tests.fsproj" +$identityApp = "./samples/IdentityApp/IdentityApp/IdentityApp.fsproj" +$jwtApp = "./samples/JwtApp/JwtApp/JwtApp.fsproj" +$sampleApp = "./samples/SampleApp/SampleApp/SampleApp.fsproj" +$sampleAppTests = "./samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj" Update-AppVeyorBuildVersion $giraffe From 8137e26db1d88d7119960fc2662403e88a84f5d8 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Tue, 28 Aug 2018 08:49:53 +0100 Subject: [PATCH 06/43] Updated build script with latest build-functions Updated build script to make use of the latest build-functions.ps1. --- .psscripts/build-functions.ps1 | 93 ++++++++++++++++++++++++++++++---- build.ps1 | 17 ++----- 2 files changed, 88 insertions(+), 22 deletions(-) diff --git a/.psscripts/build-functions.ps1 b/.psscripts/build-functions.ps1 index 6b790d7b..a7962989 100644 --- a/.psscripts/build-functions.ps1 +++ b/.psscripts/build-functions.ps1 @@ -15,6 +15,27 @@ function Test-IsWindows [environment]::OSVersion.Platform -ne "Unix" } +function Invoke-UnsafeCmd ($cmd) +{ + <# + .DESCRIPTION + Runs a shell or bash command, but doesn't throw an error if the command didn't exit with 0. + + .PARAMETER cmd + The command to be executed. + + .EXAMPLE + Invoke-Cmd -Cmd "dotnet new classlib" + + .NOTES + Use this PowerShell command to execute any CLI commands which might not exit with 0 on a success. + #> + + Write-Host $cmd -ForegroundColor DarkCyan + if (Test-IsWindows) { $cmd = "cmd.exe /C $cmd" } + Invoke-Expression -Command $cmd +} + function Invoke-Cmd ($Cmd) { <# @@ -31,9 +52,7 @@ function Invoke-Cmd ($Cmd) Use this PowerShell command to execute any dotnet CLI commands in order to ensure that they behave the same way in the case of an error across different environments (Windows, OSX and Linux). #> - Write-Host $Cmd -ForegroundColor DarkCyan - if (Test-IsWindows) { $Cmd = "cmd.exe /C $Cmd" } - Invoke-Expression -Command $Cmd + Invoke-UnsafeCmd $cmd if ($LastExitCode -ne 0) { Write-Error "An error occured when executing '$Cmd'."; return } } @@ -52,13 +71,39 @@ function Remove-OldBuildArtifacts Remove-Item $_ -Recurse -Force } } -function Test-CompareVersions ($projFile, [string]$gitTag) +function Get-ProjectVersion ($projFile) { - Write-Host "Matching version against git tag..." -ForegroundColor Magenta + <# + .DESCRIPTION + Gets the value of a .NET Core *.csproj, *.fsproj or *.vbproj file. + + .PARAMETER cmd + The relative or absolute path to the .NET Core project file. + #> [xml]$xml = Get-Content $projFile - [string]$version = $xml.Project.PropertyGroup.Version + [string] $version = $xml.Project.PropertyGroup.Version + $version +} +function Get-NuspecVersion ($nuspecFile) +{ + <# + .DESCRIPTION + Gets the value of a .nuspec file. + + .PARAMETER cmd + The relative or absolute path to the .nuspec file. + #> + + [xml] $xml = Get-Content $nuspecFile + [string] $version = $xml.package.metadata.version + $version +} + +function Test-CompareVersions ($version, [string]$gitTag) +{ + Write-Host "Matching version against git tag..." -ForegroundColor Magenta Write-Host "Project version: $version" -ForegroundColor Cyan Write-Host "Git tag version: $gitTag" -ForegroundColor Cyan @@ -74,6 +119,7 @@ function Test-CompareVersions ($projFile, [string]$gitTag) function dotnet-info { Invoke-Cmd "dotnet --info" } function dotnet-version { Invoke-Cmd "dotnet --version" } +function dotnet-restore ($project, $argv) { Invoke-Cmd "dotnet restore $project $argv" } function dotnet-build ($project, $argv) { Invoke-Cmd "dotnet build $project $argv" } function dotnet-run ($project, $argv) { Invoke-Cmd "dotnet run --project $project $argv" } function dotnet-pack ($project, $argv) { Invoke-Cmd "dotnet pack $project $argv" } @@ -234,16 +280,43 @@ function Test-IsAppVeyorBuild { return ($env:APPVEYOR -eq $true function Test-IsAppVeyorBuildTriggeredByGitTag { return ($env:APPVEYOR_REPO_TAG -eq $true) } function Get-AppVeyorGitTag { return $env:APPVEYOR_REPO_TAG_NAME } -function Update-AppVeyorBuildVersion ($projFile) +function Update-AppVeyorBuildVersion ($version) { if (Test-IsAppVeyorBuild) { Write-Host "Updating AppVeyor build version..." -ForegroundColor Magenta - - [xml]$xml = Get-Content $projFile - $version = $xml.Project.PropertyGroup.Version $buildVersion = "$version-$env:APPVEYOR_BUILD_NUMBER" Write-Host "Setting AppVeyor build version to $buildVersion." Update-AppveyorBuild -Version $buildVersion } +} + +# ---------------------------------------------- +# Host Writing functions +# ---------------------------------------------- + +function Write-BuildHeader ($projectTitle) +{ + $header = " $projectTitle "; + $bar = "" + for ($i = 0; $i -lt $header.Length; $i++) { $bar += "-" } + + Write-Host "" + Write-Host $bar -ForegroundColor DarkYellow + Write-Host $header -ForegroundColor DarkYellow + Write-Host $bar -ForegroundColor DarkYellow + Write-Host "" +} + +function Write-SuccessFooter ($msg) +{ + $footer = " $msg "; + $bar = "" + for ($i = 0; $i -lt $footer.Length; $i++) { $bar += "-" } + + Write-Host "" + Write-Host $bar -ForegroundColor Green + Write-Host $footer -ForegroundColor Green + Write-Host $bar -ForegroundColor Green + Write-Host "" } \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index f2558f73..ec13467c 100644 --- a/build.ps1 +++ b/build.ps1 @@ -18,14 +18,10 @@ param $ErrorActionPreference = "Stop" -Write-Host "" -Write-Host "--------------------------------" -ForegroundColor DarkYellow -Write-Host " Starting Giraffe build script " -ForegroundColor DarkYellow -Write-Host "--------------------------------" -ForegroundColor DarkYellow -Write-Host "" - Import-module "$PSScriptRoot/.psscripts/build-functions.ps1" -Force +Write-BuildHeader "Starting Giraffe build script" + if ($ClearOnly.IsPresent) { Remove-OldBuildArtifacts @@ -39,7 +35,8 @@ $jwtApp = "./samples/JwtApp/JwtApp/JwtApp.fsproj" $sampleApp = "./samples/SampleApp/SampleApp/SampleApp.fsproj" $sampleAppTests = "./samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj" -Update-AppVeyorBuildVersion $giraffe +$version = Get-ProjectVersion $giraffe +Update-AppVeyorBuildVersion $version if (Test-IsAppVeyorBuildTriggeredByGitTag) { @@ -90,8 +87,4 @@ if ($Pack.IsPresent) dotnet-pack $giraffe "-c $configuration" } -Write-Host "" -Write-Host " .~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~. " -ForegroundColor Green -Write-Host " Giraffe build completed successfully! " -ForegroundColor Green -Write-Host " '~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~' " -ForegroundColor Green -Write-Host "" \ No newline at end of file +Write-SuccessFooter "Giraffe build completed successfully!" \ No newline at end of file From 2f2a6b71a68010f5df4dde4a3253964b8e0b01df Mon Sep 17 00:00:00 2001 From: Dmytro Kushnir Date: Sat, 1 Sep 2018 15:08:20 +0300 Subject: [PATCH 07/43] implemented xml and html rendering of XmlNode to String Builder added benchamrks for html rendering --- Giraffe.sln | 38 +++++-- src/Giraffe/GiraffeViewEngine.fs | 61 +++++++++++- .../Giraffe.Benchmarks.fsproj | 25 +++++ tests/Giraffe.Benchmarks/Program.fs | 98 +++++++++++++++++++ tests/Giraffe.Tests/GiraffeViewEngineTests.fs | 65 +++++++++++- 5 files changed, 273 insertions(+), 14 deletions(-) create mode 100644 tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj create mode 100644 tests/Giraffe.Benchmarks/Program.fs diff --git a/Giraffe.sln b/Giraffe.sln index 03ac8ec1..82b0b7cd 100644 --- a/Giraffe.sln +++ b/Giraffe.sln @@ -7,9 +7,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9E892FBB-74F EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{152E856C-48EA-42A1-A5F4-960819BEC170}" EndProject -Project("{f2a71f9b-5d33-465a-a702-920d77279786}") = "Giraffe", "src\Giraffe\Giraffe.fsproj", "{A16935F3-2E48-4D38-B08C-36E5ADE3B199}" +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Giraffe", "src\Giraffe\Giraffe.fsproj", "{A16935F3-2E48-4D38-B08C-36E5ADE3B199}" EndProject -Project("{f2a71f9b-5d33-465a-a702-920d77279786}") = "Giraffe.Tests", "tests\Giraffe.Tests\Giraffe.Tests.fsproj", "{2AF14B8E-56FF-4E54-99DA-C530D573814D}" +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Giraffe.Tests", "tests\Giraffe.Tests\Giraffe.Tests.fsproj", "{2AF14B8E-56FF-4E54-99DA-C530D573814D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{34A76D42-035A-4CD1-85B2-EC01D9CE3571}" EndProject @@ -45,15 +45,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IdentityApp", "IdentityApp" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GoogleAuthApp", "GoogleAuthApp", "{53C796EB-20D2-4AF9-AAB9-130150005E21}" EndProject -Project("{f2a71f9b-5d33-465a-a702-920d77279786}") = "GoogleAuthApp", "samples\GoogleAuthApp\GoogleAuthApp\GoogleAuthApp.fsproj", "{FE396475-56EA-48AC-87B8-97EF6A66612F}" +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "GoogleAuthApp", "samples\GoogleAuthApp\GoogleAuthApp\GoogleAuthApp.fsproj", "{FE396475-56EA-48AC-87B8-97EF6A66612F}" EndProject -Project("{f2a71f9b-5d33-465a-a702-920d77279786}") = "IdentityApp", "samples\IdentityApp\IdentityApp\IdentityApp.fsproj", "{3AAA2ECF-350F-4574-925F-21A909F41F42}" +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "IdentityApp", "samples\IdentityApp\IdentityApp\IdentityApp.fsproj", "{3AAA2ECF-350F-4574-925F-21A909F41F42}" EndProject -Project("{f2a71f9b-5d33-465a-a702-920d77279786}") = "JwtApp", "samples\JwtApp\JwtApp\JwtApp.fsproj", "{BCD0E9C4-62AB-45B2-8362-A7AD1E4C03A7}" +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "JwtApp", "samples\JwtApp\JwtApp\JwtApp.fsproj", "{BCD0E9C4-62AB-45B2-8362-A7AD1E4C03A7}" EndProject -Project("{f2a71f9b-5d33-465a-a702-920d77279786}") = "SampleApp", "samples\SampleApp\SampleApp\SampleApp.fsproj", "{61E7381C-C021-4048-A6F3-542E190F0D48}" +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "SampleApp", "samples\SampleApp\SampleApp\SampleApp.fsproj", "{61E7381C-C021-4048-A6F3-542E190F0D48}" EndProject -Project("{f2a71f9b-5d33-465a-a702-920d77279786}") = "SampleApp.Tests", "samples\SampleApp\SampleApp.Tests\SampleApp.Tests.fsproj", "{A878E197-31F3-4DBB-B8CC-6FBB4A53733E}" +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "SampleApp.Tests", "samples\SampleApp\SampleApp.Tests\SampleApp.Tests.fsproj", "{A878E197-31F3-4DBB-B8CC-6FBB4A53733E}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Giraffe.Benchmarks", "tests\Giraffe.Benchmarks\Giraffe.Benchmarks.fsproj", "{2188E634-B828-4629-89B9-3680422460F5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -64,9 +66,6 @@ Global Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {A16935F3-2E48-4D38-B08C-36E5ADE3B199}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A16935F3-2E48-4D38-B08C-36E5ADE3B199}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -152,6 +151,21 @@ Global {A878E197-31F3-4DBB-B8CC-6FBB4A53733E}.Release|x64.Build.0 = Release|Any CPU {A878E197-31F3-4DBB-B8CC-6FBB4A53733E}.Release|x86.ActiveCfg = Release|Any CPU {A878E197-31F3-4DBB-B8CC-6FBB4A53733E}.Release|x86.Build.0 = Release|Any CPU + {2188E634-B828-4629-89B9-3680422460F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2188E634-B828-4629-89B9-3680422460F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2188E634-B828-4629-89B9-3680422460F5}.Debug|x64.ActiveCfg = Debug|Any CPU + {2188E634-B828-4629-89B9-3680422460F5}.Debug|x64.Build.0 = Debug|Any CPU + {2188E634-B828-4629-89B9-3680422460F5}.Debug|x86.ActiveCfg = Debug|Any CPU + {2188E634-B828-4629-89B9-3680422460F5}.Debug|x86.Build.0 = Debug|Any CPU + {2188E634-B828-4629-89B9-3680422460F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2188E634-B828-4629-89B9-3680422460F5}.Release|Any CPU.Build.0 = Release|Any CPU + {2188E634-B828-4629-89B9-3680422460F5}.Release|x64.ActiveCfg = Release|Any CPU + {2188E634-B828-4629-89B9-3680422460F5}.Release|x64.Build.0 = Release|Any CPU + {2188E634-B828-4629-89B9-3680422460F5}.Release|x86.ActiveCfg = Release|Any CPU + {2188E634-B828-4629-89B9-3680422460F5}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {A16935F3-2E48-4D38-B08C-36E5ADE3B199} = {9E892FBB-74FD-464B-8939-6E4D9D70D99D} @@ -165,5 +179,9 @@ Global {BCD0E9C4-62AB-45B2-8362-A7AD1E4C03A7} = {8AC0E934-6177-4D6D-9520-6E0931E527B9} {61E7381C-C021-4048-A6F3-542E190F0D48} = {E3500770-59F9-4526-9FFA-73E725C0008F} {A878E197-31F3-4DBB-B8CC-6FBB4A53733E} = {E3500770-59F9-4526-9FFA-73E725C0008F} + {2188E634-B828-4629-89B9-3680422460F5} = {152E856C-48EA-42A1-A5F4-960819BEC170} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {77FFE315-7928-4985-B60D-8009AE1A3805} EndGlobalSection EndGlobal diff --git a/src/Giraffe/GiraffeViewEngine.fs b/src/Giraffe/GiraffeViewEngine.fs index d782c68f..45c3c363 100644 --- a/src/Giraffe/GiraffeViewEngine.fs +++ b/src/Giraffe/GiraffeViewEngine.fs @@ -42,7 +42,7 @@ type XmlNode = // Building blocks // --------------------------- -let attr (key : string) (value : string) = KeyValue (key, value) +let attr (key : string) (value : string) = KeyValue (key, WebUtility.HtmlEncode value) let flag (key : string) = Boolean key let tag (tagName : string) @@ -54,7 +54,7 @@ let voidTag (tagName : string) (attributes : XmlAttribute list) = VoidElement (tagName, Array.ofList attributes) -let encodedText (content : string) = EncodedText content +let encodedText (content : string) = RawText ( WebUtility.HtmlEncode content ) let rawText (content : string) = RawText content let emptyText = rawText "" let comment (content : string) = rawText (sprintf "" content) @@ -508,7 +508,7 @@ let rec private nodeToString (htmlStyle : bool) (node : XmlNode) = attributes |> Array.map (fun attr -> match attr with - | KeyValue (k, v) -> sprintf " %s=\"%s\"" k (WebUtility.HtmlEncode v) + | KeyValue (k, v) -> sprintf " %s=\"%s\"" k v | Boolean k -> sprintf " %s" k) |> String.Concat |> sprintf "<%s%s%s" elemName @@ -546,3 +546,58 @@ let renderHtmlDocument (document : XmlNode) = document |> renderHtmlNode |> sprintf "%s%s" Environment.NewLine + +module StatefullRendering = + open System.Text + + let inline private (+=) (sb:StringBuilder) (text:string) = sb.Append(text) + let inline private (+!) (sb:StringBuilder) (text:string) = sb.Append(text) |> ignore + + let private selfClosingBracket isHtml = + match isHtml with + | false -> " />" + | true -> ">" + + let rec private appendNodeToStringBuilder (htmlStyle : bool) (sb : StringBuilder) (node : XmlNode) : unit = + + let writeStartElement closingBracket (elemName, attributes : XmlAttribute array) = + match attributes with + | [||] -> do sb += "<" += elemName +! closingBracket + | _ -> + do sb += "<" +! elemName + attributes + |> Array.iter (fun attr -> + match attr with + | KeyValue (k, v) -> do sb += " " += k += "=\"" += v +! "\"" + | Boolean k -> do sb += " " +! k ) + do sb +! closingBracket + + let inline writeEndElement (elemName, _) = + do sb += "" + + let inline writeParentNode (elem : XmlElement, nodes : XmlNode list) = + do writeStartElement ">" elem + do List.iter (appendNodeToStringBuilder htmlStyle sb) nodes + do writeEndElement elem + + match node with + | EncodedText text -> do sb +! (WebUtility.HtmlEncode text) + | RawText text -> do sb +! text + | ParentNode (e, nodes) -> do writeParentNode (e, nodes) + | VoidElement e -> do writeStartElement (selfClosingBracket htmlStyle) e + + let renderXmlNode = + appendNodeToStringBuilder false + + let renderXmlNodes sb (nodes : XmlNode list) = + nodes |> List.iter (renderXmlNode sb) + + let renderHtmlNode = + appendNodeToStringBuilder true + + let renderHtmlNodes sb (nodes : XmlNode list) = + nodes |> List.iter (renderHtmlNode sb) + + let renderHtmlDocument sb (document : XmlNode) = + sb += "" +! Environment.NewLine + renderHtmlNode sb document \ No newline at end of file diff --git a/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj b/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj new file mode 100644 index 00000000..30421070 --- /dev/null +++ b/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj @@ -0,0 +1,25 @@ + + + + Exe + netcoreapp2.0;netcoreapp2.1 + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Giraffe.Benchmarks/Program.fs b/tests/Giraffe.Benchmarks/Program.fs new file mode 100644 index 00000000..cdb4ac13 --- /dev/null +++ b/tests/Giraffe.Benchmarks/Program.fs @@ -0,0 +1,98 @@ +open System +open BenchmarkDotNet.Attributes; +open BenchmarkDotNet.Running; +open Giraffe.GiraffeViewEngine +open System.Text + +let private DefaultCapacity = 8 * 1024 +let private MaxBuilderSize = DefaultCapacity * 3 + +type MemoryStreamCache = + + [] + [] + static val mutable private instance: StringBuilder + + static member Get() = MemoryStreamCache.Get(DefaultCapacity) + static member Get(capacity:int) = + + if capacity <= MaxBuilderSize then + let ms = MemoryStreamCache.instance; + let capacity = max capacity DefaultCapacity + + if ms <> null && capacity <= ms.Capacity then + MemoryStreamCache.instance <- null; + ms.Clear() + else + new StringBuilder(capacity) + else + new StringBuilder(capacity) + + static member Release(ms:StringBuilder) = + if ms.Capacity <= MaxBuilderSize then + MemoryStreamCache.instance <- ms + +[] +type HtmlBench() = + + let doc = + div [] [ + div [ _class "top-bar" ] + [ div [ _class "top-bar-left" ] + [ ul [ _class "dropdown menu" + _data "dropdown-menu" ] + [ li [ _class "menu-text" ] + [ rawText "Site Title" ] + li [ ] + [ a [ _href "#" ] + [ encodedText """One """ ] + ul [ _class "menu vertical" ] + [ li [ ] + [ a [ _href "#" ] + [ rawText "One" ] ] + li [ ] + [ a [ _href "#" ] + [ encodedText "Two" ] ] + li [ ] + [ a [ _href "#" ] + [ rawText "Three" ] ] ] ] + li [ ] + [ a [ _href "#" ] + [ encodedText "Two" ] ] + li [ ] + [ a [ _href "#" ] + [ encodedText "Three" ] ] ] ] + div [ _class "top-bar-right" ] + [ ul [ _class "menu" ] + [ li [ ] + [ input [ _type "search" + _placeholder "Search" ] ] + li [ ] + [ button [ _type "button" + _class "button" ] + [ rawText "Search" ] ] ] ] ] + ] + + [] + member this.RenderHtmlOriginal() = + renderHtmlDocument doc + + [] + member this.RenderHtmlStatefull() = + let sb = new StringBuilder() + StatefullRendering.renderHtmlDocument sb doc + sb.ToString() |> ignore + + [] + member this.RenderHtmlStatefullCached() = + let sb = MemoryStreamCache.Get() + StatefullRendering.renderHtmlDocument sb doc + sb.ToString() |> ignore + MemoryStreamCache.Release sb + +[] +let main args = + let asm = typeof.Assembly + BenchmarkSwitcher.FromAssembly(asm).Run(args) |> ignore + 0 + diff --git a/tests/Giraffe.Tests/GiraffeViewEngineTests.fs b/tests/Giraffe.Tests/GiraffeViewEngineTests.fs index 89f0340e..84a1f47d 100644 --- a/tests/Giraffe.Tests/GiraffeViewEngineTests.fs +++ b/tests/Giraffe.Tests/GiraffeViewEngineTests.fs @@ -2,6 +2,7 @@ module Giraffe.Tests.GiraffeViewEngineTests open Xunit open Giraffe.GiraffeViewEngine +open System.Text [] let ``Single html root should compile`` () = @@ -51,4 +52,66 @@ let ``Void tag in XML should be self closing tag`` () = [] let ``Void tag in HTML should be unary tag`` () = let unary = br [] |> renderHtmlNode - Assert.Equal("
", unary) \ No newline at end of file + Assert.Equal("
", unary) + + +let doc = + div [] [ + div [ _class "top-bar" ] + [ div [ _class "top-bar-left" ] + [ ul [ _class "dropdown menu" + _data "dropdown-menu" ] + [ li [ _class "menu-text" ] + [ RawText "Site Title" ] + li [ ] + [ a [ _href "#" ] + [ EncodedText """One """ ] + ul [ _class "menu vertical" ] + [ li [ ] + [ a [ _href "#" ] + [ RawText "One" ] ] + li [ ] + [ a [ _href "#" ] + [ EncodedText "Two" ] ] + li [ ] + [ a [ _href "#" ] + [ RawText "Three" ] ] ] ] + li [ ] + [ a [ _href "#" ] + [ EncodedText "Two" ] ] + li [ ] + [ a [ _href "#" ] + [ EncodedText "Three" ] ] ] ] + div [ _class "top-bar-right" ] + [ ul [ _class "menu" ] + [ li [ ] + [ input [ _type "search" + _placeholder "Search" ] ] + li [ ] + [ button [ _type "button" + _class "button" ] + [ RawText "Search" ] ] ] ] ] + ] + +[] +let ``Statefull rendering produces same result as original implementation when rendering HTML`` () = + + let original = renderHtmlDocument doc + + let sb = StringBuilder() + StatefullRendering.renderHtmlDocument sb doc + let statefull = sb.ToString() + + Assert.Equal (original, statefull) + + +[] +let ``Statefull rendering produces same result as original implementation when rendering XML`` () = + + let original = renderXmlNode doc + + let sb = StringBuilder() + StatefullRendering.renderXmlNode sb doc + let statefull = sb.ToString() + + Assert.Equal (original, statefull) From abc56425d623db63e3184dd80652f4bbf92446d5 Mon Sep 17 00:00:00 2001 From: Dmytro Kushnir Date: Sat, 1 Sep 2018 15:49:58 +0300 Subject: [PATCH 08/43] micro tuning --- src/Giraffe/GiraffeViewEngine.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Giraffe/GiraffeViewEngine.fs b/src/Giraffe/GiraffeViewEngine.fs index 45c3c363..93bfeb65 100644 --- a/src/Giraffe/GiraffeViewEngine.fs +++ b/src/Giraffe/GiraffeViewEngine.fs @@ -553,14 +553,14 @@ module StatefullRendering = let inline private (+=) (sb:StringBuilder) (text:string) = sb.Append(text) let inline private (+!) (sb:StringBuilder) (text:string) = sb.Append(text) |> ignore - let private selfClosingBracket isHtml = + let inline private selfClosingBracket isHtml = match isHtml with | false -> " />" | true -> ">" let rec private appendNodeToStringBuilder (htmlStyle : bool) (sb : StringBuilder) (node : XmlNode) : unit = - let writeStartElement closingBracket (elemName, attributes : XmlAttribute array) = + let writeStartElement (sb : StringBuilder) closingBracket (elemName, attributes : XmlAttribute array) = match attributes with | [||] -> do sb += "<" += elemName +! closingBracket | _ -> @@ -576,7 +576,7 @@ module StatefullRendering = do sb += "" let inline writeParentNode (elem : XmlElement, nodes : XmlNode list) = - do writeStartElement ">" elem + do writeStartElement sb ">" elem do List.iter (appendNodeToStringBuilder htmlStyle sb) nodes do writeEndElement elem @@ -584,7 +584,7 @@ module StatefullRendering = | EncodedText text -> do sb +! (WebUtility.HtmlEncode text) | RawText text -> do sb +! text | ParentNode (e, nodes) -> do writeParentNode (e, nodes) - | VoidElement e -> do writeStartElement (selfClosingBracket htmlStyle) e + | VoidElement e -> do writeStartElement sb (selfClosingBracket htmlStyle) e let renderXmlNode = appendNodeToStringBuilder false From 9fa96ff6002edf44f8cb4de59d06188465748112 Mon Sep 17 00:00:00 2001 From: Dmytro Kushnir Date: Sat, 1 Sep 2018 17:18:47 +0300 Subject: [PATCH 09/43] remove allocations on List.iter --- src/Giraffe/GiraffeViewEngine.fs | 17 +++++++++++------ .../Giraffe.Benchmarks.fsproj | 2 +- tests/Giraffe.Benchmarks/Program.fs | 8 +++++++- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Giraffe/GiraffeViewEngine.fs b/src/Giraffe/GiraffeViewEngine.fs index 93bfeb65..e2a44108 100644 --- a/src/Giraffe/GiraffeViewEngine.fs +++ b/src/Giraffe/GiraffeViewEngine.fs @@ -560,31 +560,36 @@ module StatefullRendering = let rec private appendNodeToStringBuilder (htmlStyle : bool) (sb : StringBuilder) (node : XmlNode) : unit = - let writeStartElement (sb : StringBuilder) closingBracket (elemName, attributes : XmlAttribute array) = + let writeStartElement closingBracket (elemName, attributes : XmlAttribute array) = match attributes with | [||] -> do sb += "<" += elemName +! closingBracket | _ -> do sb += "<" +! elemName + attributes |> Array.iter (fun attr -> match attr with | KeyValue (k, v) -> do sb += " " += k += "=\"" += v +! "\"" | Boolean k -> do sb += " " +! k ) + do sb +! closingBracket let inline writeEndElement (elemName, _) = do sb += "" - let inline writeParentNode (elem : XmlElement, nodes : XmlNode list) = - do writeStartElement sb ">" elem - do List.iter (appendNodeToStringBuilder htmlStyle sb) nodes + let inline writeParentNode (elem : XmlElement) (nodes : XmlNode list) = + do writeStartElement ">" elem + + for node in nodes do + appendNodeToStringBuilder htmlStyle sb node + do writeEndElement elem match node with | EncodedText text -> do sb +! (WebUtility.HtmlEncode text) | RawText text -> do sb +! text - | ParentNode (e, nodes) -> do writeParentNode (e, nodes) - | VoidElement e -> do writeStartElement sb (selfClosingBracket htmlStyle) e + | ParentNode (e, nodes) -> do writeParentNode e nodes + | VoidElement e -> do writeStartElement (selfClosingBracket htmlStyle) e let renderXmlNode = appendNodeToStringBuilder false diff --git a/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj b/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj index 30421070..35944153 100644 --- a/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj +++ b/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj @@ -14,7 +14,7 @@
- + diff --git a/tests/Giraffe.Benchmarks/Program.fs b/tests/Giraffe.Benchmarks/Program.fs index cdb4ac13..98863385 100644 --- a/tests/Giraffe.Benchmarks/Program.fs +++ b/tests/Giraffe.Benchmarks/Program.fs @@ -4,7 +4,7 @@ open BenchmarkDotNet.Running; open Giraffe.GiraffeViewEngine open System.Text -let private DefaultCapacity = 8 * 1024 +let private DefaultCapacity = 16 * 1024 let private MaxBuilderSize = DefaultCapacity * 3 type MemoryStreamCache = @@ -90,6 +90,12 @@ type HtmlBench() = sb.ToString() |> ignore MemoryStreamCache.Release sb + [] + member this.RenderHtmlStatefullCachedNoCopy() = + let sb = MemoryStreamCache.Get() + StatefullRendering.renderHtmlDocument sb doc + MemoryStreamCache.Release sb + [] let main args = let asm = typeof.Assembly From d8b6fbe633b942a0ccb9730f10ff3cb4f0b1349f Mon Sep 17 00:00:00 2001 From: Dmitry Kushnir Date: Mon, 3 Sep 2018 14:23:00 +0300 Subject: [PATCH 10/43] added string builder cache updated html becnhmark to encode to utf8 --- src/Giraffe/ResponseWriters.fs | 48 ++++++++++++++++++++++- tests/Giraffe.Benchmarks/Program.fs | 59 +++++++++-------------------- 2 files changed, 65 insertions(+), 42 deletions(-) diff --git a/src/Giraffe/ResponseWriters.fs b/src/Giraffe/ResponseWriters.fs index 7000b9d2..a60497b3 100644 --- a/src/Giraffe/ResponseWriters.fs +++ b/src/Giraffe/ResponseWriters.fs @@ -7,6 +7,41 @@ open Microsoft.AspNetCore.Http open Microsoft.Net.Http.Headers open FSharp.Control.Tasks.V2.ContextInsensitive open Giraffe.GiraffeViewEngine +open System.Buffers + +// --------------------------- +// Internal implementation of caching +// --------------------------- + +module Caching = + open System + + let private DefaultCapacity = 8 * 1024 + let private MaxBuilderSize = DefaultCapacity * 8 + + // --------------------------- + // Holds an instance of StringBuilder of maximum capacity per thread. + // For StringBuilder of larger size will behave exactly as creating a new instance + // --------------------------- + + type StringBuilderCache = + + [] + [] + static val mutable private instance: StringBuilder + + static member Get() : StringBuilder = + let ms = StringBuilderCache.instance; + + if ms <> null && DefaultCapacity <= ms.Capacity then + StringBuilderCache.instance <- null; + ms.Clear() + else + new StringBuilder(DefaultCapacity) + + static member Release(ms:StringBuilder) : unit = + if ms.Capacity <= MaxBuilderSize then + StringBuilderCache.instance <- ms // --------------------------- // HttpContext extensions @@ -167,8 +202,19 @@ type HttpContext with /// Task of `Some HttpContext` after writing to the body of the response. /// member this.WriteHtmlViewAsync (htmlView : XmlNode) = + + let inline render htmlView : byte[] = + let sb = Caching.StringBuilderCache.Get() + StatefullRendering.renderHtmlDocument sb htmlView + let chars = ArrayPool.Shared.Rent(sb.Length) + sb.CopyTo(0, chars, 0, sb.Length) + let result = Encoding.UTF8.GetBytes(chars, 0, sb.Length) + Caching.StringBuilderCache.Release sb + ArrayPool.Shared.Return chars + result + this.SetContentType "text/html" - this.WriteStringAsync (renderHtmlDocument htmlView) + this.WriteBytesAsync <| render htmlView // --------------------------- // HttpHandler functions diff --git a/tests/Giraffe.Benchmarks/Program.fs b/tests/Giraffe.Benchmarks/Program.fs index 98863385..f512583d 100644 --- a/tests/Giraffe.Benchmarks/Program.fs +++ b/tests/Giraffe.Benchmarks/Program.fs @@ -1,36 +1,9 @@ -open System -open BenchmarkDotNet.Attributes; +open BenchmarkDotNet.Attributes; open BenchmarkDotNet.Running; open Giraffe.GiraffeViewEngine open System.Text - -let private DefaultCapacity = 16 * 1024 -let private MaxBuilderSize = DefaultCapacity * 3 - -type MemoryStreamCache = - - [] - [] - static val mutable private instance: StringBuilder - - static member Get() = MemoryStreamCache.Get(DefaultCapacity) - static member Get(capacity:int) = - - if capacity <= MaxBuilderSize then - let ms = MemoryStreamCache.instance; - let capacity = max capacity DefaultCapacity - - if ms <> null && capacity <= ms.Capacity then - MemoryStreamCache.instance <- null; - ms.Clear() - else - new StringBuilder(capacity) - else - new StringBuilder(capacity) - - static member Release(ms:StringBuilder) = - if ms.Capacity <= MaxBuilderSize then - MemoryStreamCache.instance <- ms +open System.Buffers +open Giraffe.ResponseWriters.Caching [] type HtmlBench() = @@ -74,27 +47,31 @@ type HtmlBench() = ] [] - member this.RenderHtmlOriginal() = - renderHtmlDocument doc + member this.RenderHtmlOriginalUtf8() = + renderHtmlDocument doc |> Encoding.UTF8.GetBytes [] - member this.RenderHtmlStatefull() = + member this.RenderHtmlStatefullUtf8() = let sb = new StringBuilder() StatefullRendering.renderHtmlDocument sb doc - sb.ToString() |> ignore + sb.ToString() |> Encoding.UTF8.GetBytes [] - member this.RenderHtmlStatefullCached() = - let sb = MemoryStreamCache.Get() + member this.RenderHtmlStatefullCachedUtf8() = + let sb = StringBuilderCache.Get() StatefullRendering.renderHtmlDocument sb doc - sb.ToString() |> ignore - MemoryStreamCache.Release sb + sb.ToString() |> Encoding.UTF8.GetBytes |> ignore + StringBuilderCache.Release sb [] - member this.RenderHtmlStatefullCachedNoCopy() = - let sb = MemoryStreamCache.Get() + member this.RenderHtmlStatefullCachedPooledUtf8() = + let sb = StringBuilderCache.Get() StatefullRendering.renderHtmlDocument sb doc - MemoryStreamCache.Release sb + let chars = ArrayPool.Shared.Rent(sb.Length) + sb.CopyTo(0, chars, 0, sb.Length) + Encoding.UTF8.GetBytes(chars, 0, sb.Length) |> ignore + ArrayPool.Shared.Return(chars) + StringBuilderCache.Release sb [] let main args = From fd1acc174b90739ddaa8caaebb1294b7105cae0a Mon Sep 17 00:00:00 2001 From: Dmitry Kushnir Date: Mon, 3 Sep 2018 16:40:13 +0300 Subject: [PATCH 11/43] removed StatefullRendering namespace --- src/Giraffe/GiraffeViewEngine.fs | 153 ++++++------------ src/Giraffe/ResponseWriters.fs | 23 ++- tests/Giraffe.Benchmarks/Program.fs | 47 ++++-- tests/Giraffe.Tests/GiraffeViewEngineTests.fs | 62 ------- 4 files changed, 97 insertions(+), 188 deletions(-) diff --git a/src/Giraffe/GiraffeViewEngine.fs b/src/Giraffe/GiraffeViewEngine.fs index e2a44108..03caf5ba 100644 --- a/src/Giraffe/GiraffeViewEngine.fs +++ b/src/Giraffe/GiraffeViewEngine.fs @@ -17,6 +17,7 @@ module Giraffe.GiraffeViewEngine open System open System.Net +open System.Text // --------------------------- // Definition of different HTML content @@ -488,121 +489,73 @@ module Accessibility = let _ariaValueNow = attr "aria-valuenow" let _ariaValueText = attr "aria-valuetext" - // --------------------------- -// Render XML string +// Render to string builder // --------------------------- -let rec private nodeToString (htmlStyle : bool) (node : XmlNode) = - let startElementToString selfClosing (elemName, attributes : XmlAttribute array) = - let closingBracket = - match selfClosing with - | false -> ">" - | true -> - match htmlStyle with - | false -> " />" - | true -> ">" - match attributes with - | [||] -> sprintf "<%s%s" elemName closingBracket - | _ -> - attributes - |> Array.map (fun attr -> - match attr with - | KeyValue (k, v) -> sprintf " %s=\"%s\"" k v - | Boolean k -> sprintf " %s" k) - |> String.Concat - |> sprintf "<%s%s%s" elemName - <| closingBracket - - let endElementToString (elemName, _) = sprintf "" elemName - - let parentNodeToString (elem : XmlElement, nodes : XmlNode list) = - let innerContent = nodes |> List.map (nodeToString htmlStyle) |> String.Concat - let startTag = elem |> startElementToString false - let endTag = elem |> endElementToString - sprintf "%s%s%s" startTag innerContent endTag - - match node with - | EncodedText text -> WebUtility.HtmlEncode text - | RawText text -> text - | ParentNode (e, nodes) -> parentNodeToString (e, nodes) - | VoidElement e -> startElementToString true e - -let renderXmlNode = nodeToString false - -let renderXmlNodes (nodes : XmlNode list) = - nodes - |> List.map renderXmlNode - |> String.Concat - -let renderHtmlNode = nodeToString true - -let renderHtmlNodes (nodes : XmlNode list) = - nodes - |> List.map renderHtmlNode - |> String.Concat +let inline private (+=) (sb: StringBuilder) (text: string) = sb.Append(text) +let inline private (+!) (sb: StringBuilder) (text: string) = sb.Append(text) |> ignore -let renderHtmlDocument (document : XmlNode) = - document - |> renderHtmlNode - |> sprintf "%s%s" Environment.NewLine +let inline private selfClosingBracket isHtml = + match isHtml with + | false -> " />" + | true -> ">" -module StatefullRendering = - open System.Text - - let inline private (+=) (sb:StringBuilder) (text:string) = sb.Append(text) - let inline private (+!) (sb:StringBuilder) (text:string) = sb.Append(text) |> ignore - - let inline private selfClosingBracket isHtml = - match isHtml with - | false -> " />" - | true -> ">" - - let rec private appendNodeToStringBuilder (htmlStyle : bool) (sb : StringBuilder) (node : XmlNode) : unit = +let rec private appendNodeToStringBuilder (htmlStyle: bool) (sb: StringBuilder) (node: XmlNode) : unit = - let writeStartElement closingBracket (elemName, attributes : XmlAttribute array) = - match attributes with - | [||] -> do sb += "<" += elemName +! closingBracket - | _ -> - do sb += "<" +! elemName + let writeStartElement closingBracket (elemName, attributes : XmlAttribute array) = + match attributes with + | [||] -> do sb += "<" += elemName +! closingBracket + | _ -> + do sb += "<" +! elemName - attributes - |> Array.iter (fun attr -> - match attr with - | KeyValue (k, v) -> do sb += " " += k += "=\"" += v +! "\"" - | Boolean k -> do sb += " " +! k ) + attributes + |> Array.iter (fun attr -> + match attr with + | KeyValue (k, v) -> do sb += " " += k += "=\"" += v +! "\"" + | Boolean k -> do sb += " " +! k ) - do sb +! closingBracket + do sb +! closingBracket - let inline writeEndElement (elemName, _) = - do sb += "" + let inline writeEndElement (elemName, _) = + do sb += "" - let inline writeParentNode (elem : XmlElement) (nodes : XmlNode list) = - do writeStartElement ">" elem + let inline writeParentNode (elem : XmlElement) (nodes : XmlNode list) = + do writeStartElement ">" elem - for node in nodes do - appendNodeToStringBuilder htmlStyle sb node + for node in nodes do + appendNodeToStringBuilder htmlStyle sb node - do writeEndElement elem + do writeEndElement elem - match node with - | EncodedText text -> do sb +! (WebUtility.HtmlEncode text) - | RawText text -> do sb +! text - | ParentNode (e, nodes) -> do writeParentNode e nodes - | VoidElement e -> do writeStartElement (selfClosingBracket htmlStyle) e + match node with + | EncodedText text -> do sb +! (WebUtility.HtmlEncode text) + | RawText text -> do sb +! text + | ParentNode (e, nodes) -> do writeParentNode e nodes + | VoidElement e -> do writeStartElement (selfClosingBracket htmlStyle) e - let renderXmlNode = - appendNodeToStringBuilder false +let renderXmlNode' sb node = appendNodeToStringBuilder false sb node - let renderXmlNodes sb (nodes : XmlNode list) = - nodes |> List.iter (renderXmlNode sb) +let renderXmlNodes' sb (nodes : XmlNode list) = + for node in nodes do + renderXmlNode' sb node + +let renderHtmlNode' sb node = appendNodeToStringBuilder true sb node + +let renderHtmlNodes' sb (nodes : XmlNode list) = + for node in nodes do + renderHtmlNode' sb node - let renderHtmlNode = - appendNodeToStringBuilder true +let renderHtmlDocument' sb (document : XmlNode) = + sb += "" +! Environment.NewLine + renderHtmlNode' sb document - let renderHtmlNodes sb (nodes : XmlNode list) = - nodes |> List.iter (renderHtmlNode sb) +// --------------------------- +// Render XML string +// --------------------------- - let renderHtmlDocument sb (document : XmlNode) = - sb += "" +! Environment.NewLine - renderHtmlNode sb document \ No newline at end of file +let renderXmlNode (node: XmlNode): string = let sb = new StringBuilder() in renderXmlNode' sb node; sb.ToString() +let renderXmlNodes (nodes: XmlNode list): string = let sb = new StringBuilder() in renderXmlNodes' sb nodes; sb.ToString() +let renderHtmlNode (node:XmlNode): string = let sb = new StringBuilder() in renderHtmlNode' sb node; sb.ToString() +let renderHtmlNodes (nodes: XmlNode list): string = let sb = new StringBuilder() in renderHtmlNodes' sb nodes; sb.ToString() +let renderHtmlDocument (document: XmlNode): string = let sb = new StringBuilder() in renderHtmlDocument' sb document; sb.ToString() diff --git a/src/Giraffe/ResponseWriters.fs b/src/Giraffe/ResponseWriters.fs index a60497b3..046211cd 100644 --- a/src/Giraffe/ResponseWriters.fs +++ b/src/Giraffe/ResponseWriters.fs @@ -1,6 +1,7 @@ [] module Giraffe.ResponseWriters +open System open System.IO open System.Text open Microsoft.AspNetCore.Http @@ -10,20 +11,16 @@ open Giraffe.GiraffeViewEngine open System.Buffers // --------------------------- -// Internal implementation of caching +// Internal implementation of string builder caching // --------------------------- -module Caching = - open System +module private Caching = - let private DefaultCapacity = 8 * 1024 - let private MaxBuilderSize = DefaultCapacity * 8 - - // --------------------------- - // Holds an instance of StringBuilder of maximum capacity per thread. - // For StringBuilder of larger size will behave exactly as creating a new instance - // --------------------------- + let DefaultCapacity = 8 * 1024 + let MaxBuilderSize = DefaultCapacity * 8 + /// Holds an instance of StringBuilder of maximum capacity per thread. + /// For `StringBuilder` of larger than `MaxBuilderSize` will behave as `new StringBuilder()` constructor call type StringBuilderCache = [] @@ -203,9 +200,11 @@ type HttpContext with /// member this.WriteHtmlViewAsync (htmlView : XmlNode) = - let inline render htmlView : byte[] = + /// renders html document to cached string builder instance + /// and converts it to the utf8 byte array + let inline render (htmlView: XmlNode): byte[] = let sb = Caching.StringBuilderCache.Get() - StatefullRendering.renderHtmlDocument sb htmlView + renderHtmlDocument' sb htmlView |> ignore let chars = ArrayPool.Shared.Rent(sb.Length) sb.CopyTo(0, chars, 0, sb.Length) let result = Encoding.UTF8.GetBytes(chars, 0, sb.Length) diff --git a/tests/Giraffe.Benchmarks/Program.fs b/tests/Giraffe.Benchmarks/Program.fs index f512583d..0c090a4b 100644 --- a/tests/Giraffe.Benchmarks/Program.fs +++ b/tests/Giraffe.Benchmarks/Program.fs @@ -3,10 +3,35 @@ open BenchmarkDotNet.Running; open Giraffe.GiraffeViewEngine open System.Text open System.Buffers -open Giraffe.ResponseWriters.Caching +open System + +[] +module Caching = + + let DefaultCapacity = 8 * 1024 + let MaxBuilderSize = DefaultCapacity * 8 + + type StringBuilderCache = + + [] + [] + static val mutable private instance: StringBuilder + + static member Get() : StringBuilder = + let ms = StringBuilderCache.instance; + + if ms <> null && DefaultCapacity <= ms.Capacity then + StringBuilderCache.instance <- null; + ms.Clear() + else + new StringBuilder(DefaultCapacity) + + static member Release(ms:StringBuilder) : unit = + if ms.Capacity <= MaxBuilderSize then + StringBuilderCache.instance <- ms [] -type HtmlBench() = +type HtmlUtf8Benchmark() = let doc = div [] [ @@ -47,26 +72,20 @@ type HtmlBench() = ] [] - member this.RenderHtmlOriginalUtf8() = + member this.String() = renderHtmlDocument doc |> Encoding.UTF8.GetBytes [] - member this.RenderHtmlStatefullUtf8() = - let sb = new StringBuilder() - StatefullRendering.renderHtmlDocument sb doc - sb.ToString() |> Encoding.UTF8.GetBytes - - [] - member this.RenderHtmlStatefullCachedUtf8() = + member this.Cached() = let sb = StringBuilderCache.Get() - StatefullRendering.renderHtmlDocument sb doc + renderHtmlDocument' sb doc sb.ToString() |> Encoding.UTF8.GetBytes |> ignore StringBuilderCache.Release sb [] - member this.RenderHtmlStatefullCachedPooledUtf8() = + member this.CachedAndPooled() = let sb = StringBuilderCache.Get() - StatefullRendering.renderHtmlDocument sb doc + renderHtmlDocument' sb doc let chars = ArrayPool.Shared.Rent(sb.Length) sb.CopyTo(0, chars, 0, sb.Length) Encoding.UTF8.GetBytes(chars, 0, sb.Length) |> ignore @@ -75,7 +94,7 @@ type HtmlBench() = [] let main args = - let asm = typeof.Assembly + let asm = typeof.Assembly BenchmarkSwitcher.FromAssembly(asm).Run(args) |> ignore 0 diff --git a/tests/Giraffe.Tests/GiraffeViewEngineTests.fs b/tests/Giraffe.Tests/GiraffeViewEngineTests.fs index 84a1f47d..60859941 100644 --- a/tests/Giraffe.Tests/GiraffeViewEngineTests.fs +++ b/tests/Giraffe.Tests/GiraffeViewEngineTests.fs @@ -53,65 +53,3 @@ let ``Void tag in XML should be self closing tag`` () = let ``Void tag in HTML should be unary tag`` () = let unary = br [] |> renderHtmlNode Assert.Equal("
", unary) - - -let doc = - div [] [ - div [ _class "top-bar" ] - [ div [ _class "top-bar-left" ] - [ ul [ _class "dropdown menu" - _data "dropdown-menu" ] - [ li [ _class "menu-text" ] - [ RawText "Site Title" ] - li [ ] - [ a [ _href "#" ] - [ EncodedText """One """ ] - ul [ _class "menu vertical" ] - [ li [ ] - [ a [ _href "#" ] - [ RawText "One" ] ] - li [ ] - [ a [ _href "#" ] - [ EncodedText "Two" ] ] - li [ ] - [ a [ _href "#" ] - [ RawText "Three" ] ] ] ] - li [ ] - [ a [ _href "#" ] - [ EncodedText "Two" ] ] - li [ ] - [ a [ _href "#" ] - [ EncodedText "Three" ] ] ] ] - div [ _class "top-bar-right" ] - [ ul [ _class "menu" ] - [ li [ ] - [ input [ _type "search" - _placeholder "Search" ] ] - li [ ] - [ button [ _type "button" - _class "button" ] - [ RawText "Search" ] ] ] ] ] - ] - -[] -let ``Statefull rendering produces same result as original implementation when rendering HTML`` () = - - let original = renderHtmlDocument doc - - let sb = StringBuilder() - StatefullRendering.renderHtmlDocument sb doc - let statefull = sb.ToString() - - Assert.Equal (original, statefull) - - -[] -let ``Statefull rendering produces same result as original implementation when rendering XML`` () = - - let original = renderXmlNode doc - - let sb = StringBuilder() - StatefullRendering.renderXmlNode sb doc - let statefull = sb.ToString() - - Assert.Equal (original, statefull) From ada633f225dbedd0fe5089c776d862956c6acd26 Mon Sep 17 00:00:00 2001 From: Dmytro Kushnir Date: Thu, 6 Sep 2018 01:31:51 +0300 Subject: [PATCH 12/43] unused ref --- src/Giraffe/Middleware.fs | 1 + src/Giraffe/ResponseWriters.fs | 39 +++--------------- src/Giraffe/Serialization.fs | 40 ++++++++++++++++++- tests/Giraffe.Benchmarks/Program.fs | 37 ++++------------- tests/Giraffe.Tests/Helpers.fs | 6 +++ .../HttpContextExtensionsTests.fs | 1 + tests/Giraffe.Tests/HttpHandlerTests.fs | 2 + 7 files changed, 62 insertions(+), 64 deletions(-) diff --git a/src/Giraffe/Middleware.fs b/src/Giraffe/Middleware.fs index 56237b31..678abf35 100644 --- a/src/Giraffe/Middleware.fs +++ b/src/Giraffe/Middleware.fs @@ -122,6 +122,7 @@ type IServiceCollection with /// Returns an `IServiceCollection` builder object. /// member this.AddGiraffe() = + this.TryAddSingleton(NoOpStringBuilderCache(1024)) this.TryAddSingleton(NewtonsoftJsonSerializer(NewtonsoftJsonSerializer.DefaultSettings)) this.TryAddSingleton(DefaultXmlSerializer(DefaultXmlSerializer.DefaultSettings)) this.TryAddSingleton() diff --git a/src/Giraffe/ResponseWriters.fs b/src/Giraffe/ResponseWriters.fs index 046211cd..312e2091 100644 --- a/src/Giraffe/ResponseWriters.fs +++ b/src/Giraffe/ResponseWriters.fs @@ -9,36 +9,7 @@ open Microsoft.Net.Http.Headers open FSharp.Control.Tasks.V2.ContextInsensitive open Giraffe.GiraffeViewEngine open System.Buffers - -// --------------------------- -// Internal implementation of string builder caching -// --------------------------- - -module private Caching = - - let DefaultCapacity = 8 * 1024 - let MaxBuilderSize = DefaultCapacity * 8 - - /// Holds an instance of StringBuilder of maximum capacity per thread. - /// For `StringBuilder` of larger than `MaxBuilderSize` will behave as `new StringBuilder()` constructor call - type StringBuilderCache = - - [] - [] - static val mutable private instance: StringBuilder - - static member Get() : StringBuilder = - let ms = StringBuilderCache.instance; - - if ms <> null && DefaultCapacity <= ms.Capacity then - StringBuilderCache.instance <- null; - ms.Clear() - else - new StringBuilder(DefaultCapacity) - - static member Release(ms:StringBuilder) : unit = - if ms.Capacity <= MaxBuilderSize then - StringBuilderCache.instance <- ms +open Giraffe.Serialization.Cache // --------------------------- // HttpContext extensions @@ -200,15 +171,17 @@ type HttpContext with /// member this.WriteHtmlViewAsync (htmlView : XmlNode) = + let stringBuilderCache = this.GetService() + /// renders html document to cached string builder instance /// and converts it to the utf8 byte array let inline render (htmlView: XmlNode): byte[] = - let sb = Caching.StringBuilderCache.Get() - renderHtmlDocument' sb htmlView |> ignore + let sb = stringBuilderCache.Get() + renderHtmlDocument' sb htmlView let chars = ArrayPool.Shared.Rent(sb.Length) sb.CopyTo(0, chars, 0, sb.Length) let result = Encoding.UTF8.GetBytes(chars, 0, sb.Length) - Caching.StringBuilderCache.Release sb + stringBuilderCache.Release sb ArrayPool.Shared.Return chars result diff --git a/src/Giraffe/Serialization.fs b/src/Giraffe/Serialization.fs index 9703cc23..04961b5f 100644 --- a/src/Giraffe/Serialization.fs +++ b/src/Giraffe/Serialization.fs @@ -98,4 +98,42 @@ module Xml = member __.Deserialize<'T> (xml : string) = let serializer = XmlSerializer(typeof<'T>) use reader = new StringReader(xml) - serializer.Deserialize reader :?> 'T \ No newline at end of file + serializer.Deserialize reader :?> 'T + +[] +module Cache = + open System.Text + open System + + type IStringBuilderCache = + abstract member Get: unit -> StringBuilder + abstract member Release: StringBuilder -> unit + + type NoOpStringBuilderCache(DefaultCapacity: int) = + interface IStringBuilderCache with + member this.Get() = new StringBuilder(DefaultCapacity) + member this.Release _ = (); + + type private StringBuilderCache = + + [] + [] + static val mutable private instance: StringBuilder + + static member Get(defaultCapacity) : StringBuilder = + let sb = StringBuilderCache.instance; + + if sb <> null && sb.Capacity >= defaultCapacity then + StringBuilderCache.instance <- null; + sb.Clear() + else + new StringBuilder(defaultCapacity) + + static member Release(sb: StringBuilder, maxBuilderSize) : unit = + if sb.Capacity <= maxBuilderSize then + StringBuilderCache.instance <- sb + + type ThreadStaticStringBuilderCache(DefaultCapacity, MaxCapacity) = + interface IStringBuilderCache with + member this.Get() = StringBuilderCache.Get DefaultCapacity + member this.Release sb = StringBuilderCache.Release(sb, MaxCapacity) \ No newline at end of file diff --git a/tests/Giraffe.Benchmarks/Program.fs b/tests/Giraffe.Benchmarks/Program.fs index 0c090a4b..5a9a0dc4 100644 --- a/tests/Giraffe.Benchmarks/Program.fs +++ b/tests/Giraffe.Benchmarks/Program.fs @@ -3,36 +3,13 @@ open BenchmarkDotNet.Running; open Giraffe.GiraffeViewEngine open System.Text open System.Buffers -open System - -[] -module Caching = - - let DefaultCapacity = 8 * 1024 - let MaxBuilderSize = DefaultCapacity * 8 - - type StringBuilderCache = - - [] - [] - static val mutable private instance: StringBuilder - - static member Get() : StringBuilder = - let ms = StringBuilderCache.instance; - - if ms <> null && DefaultCapacity <= ms.Capacity then - StringBuilderCache.instance <- null; - ms.Clear() - else - new StringBuilder(DefaultCapacity) - - static member Release(ms:StringBuilder) : unit = - if ms.Capacity <= MaxBuilderSize then - StringBuilderCache.instance <- ms +open Giraffe.Serialization.Cache [] type HtmlUtf8Benchmark() = + let cache = new ThreadStaticStringBuilderCache(1024, 32 * 1024) :> IStringBuilderCache + let doc = div [] [ div [ _class "top-bar" ] @@ -77,20 +54,20 @@ type HtmlUtf8Benchmark() = [] member this.Cached() = - let sb = StringBuilderCache.Get() + let sb = cache.Get() renderHtmlDocument' sb doc sb.ToString() |> Encoding.UTF8.GetBytes |> ignore - StringBuilderCache.Release sb + cache.Release sb [] member this.CachedAndPooled() = - let sb = StringBuilderCache.Get() + let sb = cache.Get() renderHtmlDocument' sb doc let chars = ArrayPool.Shared.Rent(sb.Length) sb.CopyTo(0, chars, 0, sb.Length) Encoding.UTF8.GetBytes(chars, 0, sb.Length) |> ignore ArrayPool.Shared.Return(chars) - StringBuilderCache.Release sb + cache.Release sb [] let main args = diff --git a/tests/Giraffe.Tests/Helpers.fs b/tests/Giraffe.Tests/Helpers.fs index 4ede18a4..6c03f293 100644 --- a/tests/Giraffe.Tests/Helpers.fs +++ b/tests/Giraffe.Tests/Helpers.fs @@ -97,6 +97,12 @@ let createHost (configureApp : 'Tuple -> IApplicationBuilder -> unit) .Configure(Action (configureApp args)) .ConfigureServices(Action configureServices) +let mockCache (ctx : HttpContext) = + ctx.RequestServices + .GetService(typeof) + .Returns(NoOpStringBuilderCache(1024)) + |> ignore + let mockJson (ctx : HttpContext) (settings : JsonSerializerSettings option) = let jsonSettings = defaultArg settings NewtonsoftJsonSerializer.DefaultSettings diff --git a/tests/Giraffe.Tests/HttpContextExtensionsTests.fs b/tests/Giraffe.Tests/HttpContextExtensionsTests.fs index 2e5418f6..f7e78fe7 100644 --- a/tests/Giraffe.Tests/HttpContextExtensionsTests.fs +++ b/tests/Giraffe.Tests/HttpContextExtensionsTests.fs @@ -78,6 +78,7 @@ let ``TryGetQueryStringValue during HTTP GET request with query string returns c [] let ``WriteHtmlViewAsync should add html to the context`` () = let ctx = Substitute.For() + mockCache ctx let testHandler = fun (next : HttpFunc) (ctx : HttpContext) -> diff --git a/tests/Giraffe.Tests/HttpHandlerTests.fs b/tests/Giraffe.Tests/HttpHandlerTests.fs index 29c670da..2f9c5476 100644 --- a/tests/Giraffe.Tests/HttpHandlerTests.fs +++ b/tests/Giraffe.Tests/HttpHandlerTests.fs @@ -310,6 +310,8 @@ let ``POST "/either" with unsupported Accept header returns 404 "Not found"`` () [] let ``GET "/person" returns rendered HTML view`` () = let ctx = Substitute.For() + mockCache ctx + let personView model = html [] [ head [] [ From 1ad2f6fe2d04309b1e817c4bae2b329d1ea5a306 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Thu, 6 Sep 2018 08:46:16 +0100 Subject: [PATCH 13/43] Small improvements to the new GiraffeViewEngine Reduced a bit of the code (e.g. removed RawText and EncodedText and replaced with just Text, etc.) --- src/Giraffe/GiraffeViewEngine.fs | 104 ++++++++++++++++--------------- src/Giraffe/ResponseWriters.fs | 14 ++--- 2 files changed, 61 insertions(+), 57 deletions(-) diff --git a/src/Giraffe/GiraffeViewEngine.fs b/src/Giraffe/GiraffeViewEngine.fs index 03caf5ba..b798d5e0 100644 --- a/src/Giraffe/GiraffeViewEngine.fs +++ b/src/Giraffe/GiraffeViewEngine.fs @@ -36,14 +36,19 @@ type XmlElement = string * XmlAttribute[] // Name * XML attributes type XmlNode = | ParentNode of XmlElement * XmlNode list // An XML element which contains nested XML elements | VoidElement of XmlElement // An XML element which cannot contain nested XML (e.g.
or
) - | EncodedText of string // XML encoded text content - | RawText of string // Raw text content + | Text of string // Text content + +// --------------------------- +// Helper functions +// --------------------------- + +let inline private encode v = WebUtility.HtmlEncode v // --------------------------- // Building blocks // --------------------------- -let attr (key : string) (value : string) = KeyValue (key, WebUtility.HtmlEncode value) +let attr (key : string) (value : string) = KeyValue (key, encode value) let flag (key : string) = Boolean key let tag (tagName : string) @@ -55,8 +60,8 @@ let voidTag (tagName : string) (attributes : XmlAttribute list) = VoidElement (tagName, Array.ofList attributes) -let encodedText (content : string) = RawText ( WebUtility.HtmlEncode content ) -let rawText (content : string) = RawText content +let encodedText (content : string) = Text (encode content) +let rawText (content : string) = Text content let emptyText = rawText "" let comment (content : string) = rawText (sprintf "" content) @@ -366,7 +371,7 @@ module Attributes = /// Attributes to support WAI-ARIA accessibility guidelines module Accessibility = - + // Valid role attributes // (obtained from https://www.w3.org/TR/wai-aria/#role_definitions) let _roleAlert = attr "role" "alert" @@ -437,7 +442,7 @@ module Accessibility = let _roleTree = attr "role" "tree" let _roleTreeGrid = attr "role" "treegrid" let _roleTreeItem = attr "role" "treeitem" - + // Valid aria attributes // (obtained from https://www.w3.org/TR/wai-aria/#state_prop_def) let _ariaActiveDescendant = attr "aria-activedescendant" @@ -490,72 +495,71 @@ module Accessibility = let _ariaValueText = attr "aria-valuetext" // --------------------------- -// Render to string builder +// Render to StringBuilder // --------------------------- -let inline private (+=) (sb: StringBuilder) (text: string) = sb.Append(text) -let inline private (+!) (sb: StringBuilder) (text: string) = sb.Append(text) |> ignore +let inline private (+=) (sb : StringBuilder) (text : string) = sb.Append(text) +let inline private (+!) (sb : StringBuilder) (text : string) = sb.Append(text) |> ignore -let inline private selfClosingBracket isHtml = - match isHtml with - | false -> " />" - | true -> ">" +let inline private selfClosingBracket (isHtml : bool) = + if isHtml then ">" else " />" -let rec private appendNodeToStringBuilder (htmlStyle: bool) (sb: StringBuilder) (node: XmlNode) : unit = - - let writeStartElement closingBracket (elemName, attributes : XmlAttribute array) = +let rec private buildNode (isHtml : bool) (sb : StringBuilder) (node : XmlNode) : unit = + + let buildElement closingBracket (elemName, attributes : XmlAttribute array) = match attributes with | [||] -> do sb += "<" += elemName +! closingBracket - | _ -> + | _ -> do sb += "<" +! elemName attributes |> Array.iter (fun attr -> match attr with | KeyValue (k, v) -> do sb += " " += k += "=\"" += v +! "\"" - | Boolean k -> do sb += " " +! k ) + | Boolean k -> do sb += " " +! k) do sb +! closingBracket - let inline writeEndElement (elemName, _) = + let inline buildParentNode (elemName, attributes : XmlAttribute array) (nodes : XmlNode list) = + do buildElement ">" (elemName, attributes) + for node in nodes do buildNode isHtml sb node do sb += "" - let inline writeParentNode (elem : XmlElement) (nodes : XmlNode list) = - do writeStartElement ">" elem - - for node in nodes do - appendNodeToStringBuilder htmlStyle sb node - - do writeEndElement elem - match node with - | EncodedText text -> do sb +! (WebUtility.HtmlEncode text) - | RawText text -> do sb +! text - | ParentNode (e, nodes) -> do writeParentNode e nodes - | VoidElement e -> do writeStartElement (selfClosingBracket htmlStyle) e + | Text text -> do sb +! text + | ParentNode (e, nodes) -> do buildParentNode e nodes + | VoidElement e -> do buildElement (selfClosingBracket isHtml) e -let renderXmlNode' sb node = appendNodeToStringBuilder false sb node - -let renderXmlNodes' sb (nodes : XmlNode list) = - for node in nodes do - renderXmlNode' sb node +let private buildXmlNode = buildNode false +let private buildHtmlNode = buildNode true -let renderHtmlNode' sb node = appendNodeToStringBuilder true sb node +let private buildXmlNodes sb (nodes : XmlNode list) = for n in nodes do buildXmlNode sb n +let private buildHtmlNodes sb (nodes : XmlNode list) = for n in nodes do buildHtmlNode sb n -let renderHtmlNodes' sb (nodes : XmlNode list) = - for node in nodes do - renderHtmlNode' sb node - -let renderHtmlDocument' sb (document : XmlNode) = +let buildHtmlDocument sb (document : XmlNode) = sb += "" +! Environment.NewLine - renderHtmlNode' sb document + buildHtmlNode sb document // --------------------------- -// Render XML string +// Render HTML/XML strings // --------------------------- -let renderXmlNode (node: XmlNode): string = let sb = new StringBuilder() in renderXmlNode' sb node; sb.ToString() -let renderXmlNodes (nodes: XmlNode list): string = let sb = new StringBuilder() in renderXmlNodes' sb nodes; sb.ToString() -let renderHtmlNode (node:XmlNode): string = let sb = new StringBuilder() in renderHtmlNode' sb node; sb.ToString() -let renderHtmlNodes (nodes: XmlNode list): string = let sb = new StringBuilder() in renderHtmlNodes' sb nodes; sb.ToString() -let renderHtmlDocument (document: XmlNode): string = let sb = new StringBuilder() in renderHtmlDocument' sb document; sb.ToString() +let renderXmlNode (node : XmlNode) : string = + let sb = new StringBuilder() in buildXmlNode sb node + sb.ToString() + +let renderXmlNodes (nodes : XmlNode list) : string = + let sb = new StringBuilder() in buildXmlNodes sb nodes + sb.ToString() + +let renderHtmlNode (node : XmlNode) : string = + let sb = new StringBuilder() in buildHtmlNode sb node + sb.ToString() + +let renderHtmlNodes (nodes : XmlNode list) : string = + let sb = new StringBuilder() in buildHtmlNodes sb nodes + sb.ToString() + +let renderHtmlDocument (document : XmlNode) : string = + let sb = new StringBuilder() in buildHtmlDocument sb document + sb.ToString() \ No newline at end of file diff --git a/src/Giraffe/ResponseWriters.fs b/src/Giraffe/ResponseWriters.fs index 046211cd..ea53a57d 100644 --- a/src/Giraffe/ResponseWriters.fs +++ b/src/Giraffe/ResponseWriters.fs @@ -21,22 +21,22 @@ module private Caching = /// Holds an instance of StringBuilder of maximum capacity per thread. /// For `StringBuilder` of larger than `MaxBuilderSize` will behave as `new StringBuilder()` constructor call - type StringBuilderCache = + type StringBuilderCache = [] [] static val mutable private instance: StringBuilder - static member Get() : StringBuilder = + static member Get() : StringBuilder = let ms = StringBuilderCache.instance; - + if ms <> null && DefaultCapacity <= ms.Capacity then StringBuilderCache.instance <- null; ms.Clear() else new StringBuilder(DefaultCapacity) - static member Release(ms:StringBuilder) : unit = + static member Release(ms:StringBuilder) : unit = if ms.Capacity <= MaxBuilderSize then StringBuilderCache.instance <- ms @@ -202,15 +202,15 @@ type HttpContext with /// renders html document to cached string builder instance /// and converts it to the utf8 byte array - let inline render (htmlView: XmlNode): byte[] = + let inline render (htmlView: XmlNode): byte[] = let sb = Caching.StringBuilderCache.Get() - renderHtmlDocument' sb htmlView |> ignore + buildHtmlDocument sb htmlView |> ignore let chars = ArrayPool.Shared.Rent(sb.Length) sb.CopyTo(0, chars, 0, sb.Length) let result = Encoding.UTF8.GetBytes(chars, 0, sb.Length) Caching.StringBuilderCache.Release sb ArrayPool.Shared.Return chars - result + result this.SetContentType "text/html" this.WriteBytesAsync <| render htmlView From aca071be07b5045ad9c20f7a3fadbb63e72aae78 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Fri, 7 Sep 2018 08:02:55 +0100 Subject: [PATCH 14/43] Small modifications Some ideas... will explain in the pull request. --- src/Giraffe/Giraffe.fsproj | 1 + src/Giraffe/GiraffeViewEngine.fs | 12 ++--- src/Giraffe/Middleware.fs | 3 +- src/Giraffe/ResponseWriters.fs | 48 +++--------------- src/Giraffe/Serialization.fs | 40 +-------------- src/Giraffe/StringBuilderProviders.fs | 50 +++++++++++++++++++ tests/Giraffe.Tests/GiraffeViewEngineTests.fs | 5 +- tests/Giraffe.Tests/Helpers.fs | 7 +-- .../HttpContextExtensionsTests.fs | 2 +- 9 files changed, 73 insertions(+), 95 deletions(-) create mode 100644 src/Giraffe/StringBuilderProviders.fs diff --git a/src/Giraffe/Giraffe.fsproj b/src/Giraffe/Giraffe.fsproj index 919d2ce3..e0cdd243 100644 --- a/src/Giraffe/Giraffe.fsproj +++ b/src/Giraffe/Giraffe.fsproj @@ -45,6 +45,7 @@ + diff --git a/src/Giraffe/GiraffeViewEngine.fs b/src/Giraffe/GiraffeViewEngine.fs index b798d5e0..80b0062d 100644 --- a/src/Giraffe/GiraffeViewEngine.fs +++ b/src/Giraffe/GiraffeViewEngine.fs @@ -495,7 +495,7 @@ module Accessibility = let _ariaValueText = attr "aria-valuetext" // --------------------------- -// Render to StringBuilder +// Build HTML/XML views // --------------------------- let inline private (+=) (sb : StringBuilder) (text : string) = sb.Append(text) @@ -530,18 +530,18 @@ let rec private buildNode (isHtml : bool) (sb : StringBuilder) (node : XmlNode) | ParentNode (e, nodes) -> do buildParentNode e nodes | VoidElement e -> do buildElement (selfClosingBracket isHtml) e -let private buildXmlNode = buildNode false -let private buildHtmlNode = buildNode true +let buildXmlNode = buildNode false +let buildHtmlNode = buildNode true -let private buildXmlNodes sb (nodes : XmlNode list) = for n in nodes do buildXmlNode sb n -let private buildHtmlNodes sb (nodes : XmlNode list) = for n in nodes do buildHtmlNode sb n +let buildXmlNodes sb (nodes : XmlNode list) = for n in nodes do buildXmlNode sb n +let buildHtmlNodes sb (nodes : XmlNode list) = for n in nodes do buildHtmlNode sb n let buildHtmlDocument sb (document : XmlNode) = sb += "" +! Environment.NewLine buildHtmlNode sb document // --------------------------- -// Render HTML/XML strings +// Render HTML/XML views // --------------------------- let renderXmlNode (node : XmlNode) : string = diff --git a/src/Giraffe/Middleware.fs b/src/Giraffe/Middleware.fs index 678abf35..5b3d1e2e 100644 --- a/src/Giraffe/Middleware.fs +++ b/src/Giraffe/Middleware.fs @@ -10,6 +10,7 @@ open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.DependencyInjection.Extensions open FSharp.Control.Tasks.V2.ContextInsensitive open Giraffe.Serialization +open Giraffe.StringBuilders // --------------------------- // Default middleware @@ -122,7 +123,7 @@ type IServiceCollection with /// Returns an `IServiceCollection` builder object. /// member this.AddGiraffe() = - this.TryAddSingleton(NoOpStringBuilderCache(1024)) + this.TryAddSingleton() this.TryAddSingleton(NewtonsoftJsonSerializer(NewtonsoftJsonSerializer.DefaultSettings)) this.TryAddSingleton(DefaultXmlSerializer(DefaultXmlSerializer.DefaultSettings)) this.TryAddSingleton() diff --git a/src/Giraffe/ResponseWriters.fs b/src/Giraffe/ResponseWriters.fs index f8d295dc..55210aac 100644 --- a/src/Giraffe/ResponseWriters.fs +++ b/src/Giraffe/ResponseWriters.fs @@ -4,42 +4,12 @@ module Giraffe.ResponseWriters open System open System.IO open System.Text +open System.Buffers open Microsoft.AspNetCore.Http open Microsoft.Net.Http.Headers open FSharp.Control.Tasks.V2.ContextInsensitive open Giraffe.GiraffeViewEngine -open System.Buffers -open Giraffe.Serialization.Cache - -// --------------------------- -// Internal implementation of string builder caching -// --------------------------- - -module private Caching = - - let DefaultCapacity = 8 * 1024 - let MaxBuilderSize = DefaultCapacity * 8 - - /// Holds an instance of StringBuilder of maximum capacity per thread. - /// For `StringBuilder` of larger than `MaxBuilderSize` will behave as `new StringBuilder()` constructor call - type StringBuilderCache = - - [] - [] - static val mutable private instance: StringBuilder - - static member Get() : StringBuilder = - let ms = StringBuilderCache.instance; - - if ms <> null && DefaultCapacity <= ms.Capacity then - StringBuilderCache.instance <- null; - ms.Clear() - else - new StringBuilder(DefaultCapacity) - - static member Release(ms:StringBuilder) : unit = - if ms.Capacity <= MaxBuilderSize then - StringBuilderCache.instance <- ms +open Giraffe.StringBuilders // --------------------------- // HttpContext extensions @@ -200,23 +170,17 @@ type HttpContext with /// Task of `Some HttpContext` after writing to the body of the response. /// member this.WriteHtmlViewAsync (htmlView : XmlNode) = - - let stringBuilderCache = this.GetService() - - /// renders html document to cached string builder instance - /// and converts it to the utf8 byte array - let inline render (htmlView: XmlNode): byte[] = - let sb = Caching.StringBuilderCache.Get() + let bytes = + use sbProvider = this.GetService() + let sb = sbProvider.Get() buildHtmlDocument sb htmlView |> ignore let chars = ArrayPool.Shared.Rent(sb.Length) sb.CopyTo(0, chars, 0, sb.Length) let result = Encoding.UTF8.GetBytes(chars, 0, sb.Length) - stringBuilderCache.Release sb ArrayPool.Shared.Return chars result - this.SetContentType "text/html" - this.WriteBytesAsync <| render htmlView + this.WriteBytesAsync bytes // --------------------------- // HttpHandler functions diff --git a/src/Giraffe/Serialization.fs b/src/Giraffe/Serialization.fs index 04961b5f..9703cc23 100644 --- a/src/Giraffe/Serialization.fs +++ b/src/Giraffe/Serialization.fs @@ -98,42 +98,4 @@ module Xml = member __.Deserialize<'T> (xml : string) = let serializer = XmlSerializer(typeof<'T>) use reader = new StringReader(xml) - serializer.Deserialize reader :?> 'T - -[] -module Cache = - open System.Text - open System - - type IStringBuilderCache = - abstract member Get: unit -> StringBuilder - abstract member Release: StringBuilder -> unit - - type NoOpStringBuilderCache(DefaultCapacity: int) = - interface IStringBuilderCache with - member this.Get() = new StringBuilder(DefaultCapacity) - member this.Release _ = (); - - type private StringBuilderCache = - - [] - [] - static val mutable private instance: StringBuilder - - static member Get(defaultCapacity) : StringBuilder = - let sb = StringBuilderCache.instance; - - if sb <> null && sb.Capacity >= defaultCapacity then - StringBuilderCache.instance <- null; - sb.Clear() - else - new StringBuilder(defaultCapacity) - - static member Release(sb: StringBuilder, maxBuilderSize) : unit = - if sb.Capacity <= maxBuilderSize then - StringBuilderCache.instance <- sb - - type ThreadStaticStringBuilderCache(DefaultCapacity, MaxCapacity) = - interface IStringBuilderCache with - member this.Get() = StringBuilderCache.Get DefaultCapacity - member this.Release sb = StringBuilderCache.Release(sb, MaxCapacity) \ No newline at end of file + serializer.Deserialize reader :?> 'T \ No newline at end of file diff --git a/src/Giraffe/StringBuilderProviders.fs b/src/Giraffe/StringBuilderProviders.fs new file mode 100644 index 00000000..9fcbe489 --- /dev/null +++ b/src/Giraffe/StringBuilderProviders.fs @@ -0,0 +1,50 @@ +namespace Giraffe.StringBuilders + +open System +open System.Text + +type private StringBuilderCache = + + [] + [] + static val mutable private sb : StringBuilder + + [] + [] + static val mutable private inUse : bool + + static member Get (capacity : int) (maxCapacity : int) : StringBuilder = + match StringBuilderCache.inUse with + | true -> new StringBuilder(capacity) + | false -> + StringBuilderCache.inUse <- true + + let sb = StringBuilderCache.sb + + match sb <> null && sb.Capacity >= capacity with + | true -> sb.Clear() + | false -> + let sb' = new StringBuilder(capacity) + if capacity <= maxCapacity then + StringBuilderCache.sb <- sb' + sb' + + static member Release() : unit = + StringBuilderCache.inUse <- false + +[] +module StringBuilderProvider = + + type IStringBuilderProvider = + inherit IDisposable + abstract member Get : unit -> StringBuilder + + type DefaultStringBuilderProvider() = + interface IStringBuilderProvider with + member __.Get() = new StringBuilder() + member __.Dispose() = () + + type ThreadStaticStringBuilderCache (defaultCapacity, maxCapacity) = + interface IStringBuilderProvider with + member __.Get() = StringBuilderCache.Get defaultCapacity maxCapacity + member __.Dispose() = StringBuilderCache.Release() \ No newline at end of file diff --git a/tests/Giraffe.Tests/GiraffeViewEngineTests.fs b/tests/Giraffe.Tests/GiraffeViewEngineTests.fs index 60859941..bcbe2e4c 100644 --- a/tests/Giraffe.Tests/GiraffeViewEngineTests.fs +++ b/tests/Giraffe.Tests/GiraffeViewEngineTests.fs @@ -2,7 +2,6 @@ module Giraffe.Tests.GiraffeViewEngineTests open Xunit open Giraffe.GiraffeViewEngine -open System.Text [] let ``Single html root should compile`` () = @@ -34,9 +33,9 @@ let ``Nested content should render correctly`` () = comment "this is a test" h1 [] [ encodedText "Header" ] p [] [ - EncodedText "Lorem " + rawText "Lorem " strong [] [ encodedText "Ipsum" ] - RawText " dollar" + encodedText " dollar" ] ] let html = nested diff --git a/tests/Giraffe.Tests/Helpers.fs b/tests/Giraffe.Tests/Helpers.fs index 6c03f293..b8639175 100644 --- a/tests/Giraffe.Tests/Helpers.fs +++ b/tests/Giraffe.Tests/Helpers.fs @@ -19,6 +19,7 @@ open NSubstitute open Newtonsoft.Json open Giraffe open Giraffe.Serialization +open Giraffe.StringBuilders // --------------------------------- // Common functions @@ -97,10 +98,10 @@ let createHost (configureApp : 'Tuple -> IApplicationBuilder -> unit) .Configure(Action (configureApp args)) .ConfigureServices(Action configureServices) -let mockCache (ctx : HttpContext) = +let mockCache (ctx : HttpContext) = ctx.RequestServices - .GetService(typeof) - .Returns(NoOpStringBuilderCache(1024)) + .GetService(typeof) + .Returns(new DefaultStringBuilderProvider()) |> ignore let mockJson (ctx : HttpContext) (settings : JsonSerializerSettings option) = diff --git a/tests/Giraffe.Tests/HttpContextExtensionsTests.fs b/tests/Giraffe.Tests/HttpContextExtensionsTests.fs index f7e78fe7..bdb65da2 100644 --- a/tests/Giraffe.Tests/HttpContextExtensionsTests.fs +++ b/tests/Giraffe.Tests/HttpContextExtensionsTests.fs @@ -86,7 +86,7 @@ let ``WriteHtmlViewAsync should add html to the context`` () = html [] [ head [] [] body [] [ - h1 [] [ EncodedText "Hello world" ] + h1 [] [ Text "Hello world" ] ] ] ctx.WriteHtmlViewAsync(htmlDoc) From 53f818f709b4c22c3d62a6589316dddf0b59d870 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Thu, 13 Sep 2018 13:39:39 +0100 Subject: [PATCH 15/43] Removed StringBuilderProvider Based on latest feedback on the PR removed the StringBuilder interface and classes and made the build methods publicly available. --- DOCUMENTATION.md | 48 ++++++++++-- RELEASE_NOTES.md | 12 +++ src/Giraffe/Giraffe.fsproj | 3 +- src/Giraffe/GiraffeViewEngine.fs | 74 ++++++++++--------- src/Giraffe/Middleware.fs | 2 - src/Giraffe/ResponseWriters.fs | 19 ++--- src/Giraffe/StringBuilderProviders.fs | 50 ------------- tests/Giraffe.Tests/Helpers.fs | 7 -- .../HttpContextExtensionsTests.fs | 1 - tests/Giraffe.Tests/HttpHandlerTests.fs | 1 - 10 files changed, 102 insertions(+), 115 deletions(-) delete mode 100644 src/Giraffe/StringBuilderProviders.fs diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 62b78f12..70cdf8b1 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -2531,9 +2531,9 @@ Please note that if the `permanent` flag is set to `true` then the Giraffe web a ## Giraffe View Engine -Giraffe has its own functional view engine which can be used to build rich GUIs for web applications. The single biggest and best contrast to other view engines (e.g. Razor, Liquid, etc.) is that the Giraffe View Engine is entirely functional written in normal (and compiled) F# code. +Giraffe has its own functional view engine which can be used to build rich UIs for web applications. The single biggest and best contrast to other view engines (e.g. Razor, Liquid, etc.) is that the Giraffe View Engine is entirely functional written in normal (and compiled) F# code. -This means that the Giraffe View Engine is by definition one of the most feature rich view engines, requires no disk IO to load a view and views are automatically compiled at build time. +This means that the Giraffe View Engine is by definition one of the most feature rich view engines available, requires no disk IO to load a view and views are automatically compiled at build time. The Giraffe View Engine uses traditional functions and F# record types to generate rich HTML/XML views. @@ -2556,7 +2556,7 @@ let indexView = ] ``` -A HTML element can either be a `ParentNode`, a `VoidElement` or `RawText`/`EncodedText`. +A HTML element can either be a `ParentNode`, a `VoidElement` or a `Text` element. For example the `` or `
` tags are typical `ParentNode` elements. They can hold an `XmlAttribute list` and a second `XmlElement list` for their child elements: @@ -2614,7 +2614,7 @@ Naturally the most frequent content in any HTML document is pure text:
``` -The Giraffe View Engine lets one create pure text content as either `RawText` or `EncodedText`: +The Giraffe View Engine lets one create pure text content as a `Text` element. A `Text` element can either be generated via the `rawText` or `encodedText` functions: ```fsharp let someHtml = @@ -2624,7 +2624,7 @@ let someHtml = ] ``` -The `rawText` function will create an object of type `RawText` and the `encodedText` function will output an object of type `EncodedText`. The difference is that the latter will HTML encode the value when rendering the view. +The `rawText` function will create an object of type `Text` where the content will be rendered in its original form and the `encodedText` function will output a string where the content has been HTML encoded. In this example the first `p` element will literally output the string as it is (`
Hello World
`) while the second `p` element will output the value as HTML encoded string `<div>Hello World</div>`. @@ -2736,6 +2736,44 @@ let output1 = renderHtmlNode someContent let output2 = renderXmlNode someContent ``` +Additionally with Giraffe 3.0.0 or higher there is a new module called `ViewBuilder` under the `Giraffe.GiraffeViewEngine` namespace. This module exposes additional view rendering functions which compile a view into a `StringBuilder` object instead of returning a single `string`: + +- `ViewBuilder.buildHtmlDocument` +- `ViewBuilder.buildHtmlNodes` +- `ViewBuilder.buildHtmlNode` +- `ViewBuilder.buildXmlNodes` +- `ViewBuilder.buildXmlNode` + +The `ViewBuilder.build[...]` functions can be useful if there is additional string processing required before/after composing a view by the `GiraffeViewEngine` (e.g. embedding HTML snippets in an email template, etc.). These functions also serve as the lower level building blocks of the equivalent `render[...]` functions. + +Example usage: + +```fsharp +open System.Text +open Giraffe.GiraffeViewEngine + +let someHtml = + div [] [ + tag "foo" [ attr "bar" "blah" ] [ + voidTag "otherFoo" [ flag "flag1" ] + ] + ] + +let sb = new StringBuilder() + +// Perform actions on the `sb` object... +sb.AppendLine "This is a HTML snippet inside a markdown string:" + .AppendLine "" + .AppendLine "```html" |> ignore + +let sb' = ViewBuilder.buildHtmlNode sb someHtml + +// Perform more actions on the `sb` object... +sb'.AppendLine "```" |> ignore + +let markdownOutput = sb'.ToString() +``` + ### Common View Engine Features The Giraffe View Engine doesn't have any specially built functions for commonly known features such as master pages or partial views, mainly because the nature of the view engine itself doesn't require it in most cases. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d8cfcadc..3b5cbe9c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,18 @@ Release Notes ============= +## 3.0.0 + +#### Breaking changes + +- Changed the type `XmlNode` by removing the `RawText` and `EncodedText` union cases and replaced them by a single `Text` union case. The encoding is being done at an earlier stage now when calling one of the two helper functions `rawText` and `encodedText`. + +This change should not affect the majority of Giraffe users unless you were constructing your own `XmlNode` elements which were of type `RawText` or `EncodedText`. + +#### Improvements + +- Huge performance gains by changing the underlying way of how views are being composed by the `GriaffeViewEngine`. + ## 2.0.1 Changed the `task {}` CE to load from `FSharp.Control.Tasks.V2.ContextInsensitive` instead of `FSharp.Control.Tasks.ContextInsensitive`. diff --git a/src/Giraffe/Giraffe.fsproj b/src/Giraffe/Giraffe.fsproj index e0cdd243..567de758 100644 --- a/src/Giraffe/Giraffe.fsproj +++ b/src/Giraffe/Giraffe.fsproj @@ -3,7 +3,7 @@ Giraffe - 2.0.1 + 3.0.0 A native functional ASP.NET Core web framework for F# developers. Copyright 2018 Dustin Moris Gorski Dustin Moris Gorski and contributors @@ -45,7 +45,6 @@ - diff --git a/src/Giraffe/GiraffeViewEngine.fs b/src/Giraffe/GiraffeViewEngine.fs index 80b0062d..eacdff95 100644 --- a/src/Giraffe/GiraffeViewEngine.fs +++ b/src/Giraffe/GiraffeViewEngine.fs @@ -498,68 +498,70 @@ module Accessibility = // Build HTML/XML views // --------------------------- -let inline private (+=) (sb : StringBuilder) (text : string) = sb.Append(text) -let inline private (+!) (sb : StringBuilder) (text : string) = sb.Append(text) |> ignore +[] +module ViewBuilder = + let inline private (+=) (sb : StringBuilder) (text : string) = sb.Append(text) + let inline private (+!) (sb : StringBuilder) (text : string) = sb.Append(text) |> ignore -let inline private selfClosingBracket (isHtml : bool) = - if isHtml then ">" else " />" + let inline private selfClosingBracket (isHtml : bool) = + if isHtml then ">" else " />" -let rec private buildNode (isHtml : bool) (sb : StringBuilder) (node : XmlNode) : unit = + let rec private buildNode (isHtml : bool) (sb : StringBuilder) (node : XmlNode) : unit = - let buildElement closingBracket (elemName, attributes : XmlAttribute array) = - match attributes with - | [||] -> do sb += "<" += elemName +! closingBracket - | _ -> - do sb += "<" +! elemName + let buildElement closingBracket (elemName, attributes : XmlAttribute array) = + match attributes with + | [||] -> do sb += "<" += elemName +! closingBracket + | _ -> + do sb += "<" +! elemName - attributes - |> Array.iter (fun attr -> - match attr with - | KeyValue (k, v) -> do sb += " " += k += "=\"" += v +! "\"" - | Boolean k -> do sb += " " +! k) + attributes + |> Array.iter (fun attr -> + match attr with + | KeyValue (k, v) -> do sb += " " += k += "=\"" += v +! "\"" + | Boolean k -> do sb += " " +! k) - do sb +! closingBracket + do sb +! closingBracket - let inline buildParentNode (elemName, attributes : XmlAttribute array) (nodes : XmlNode list) = - do buildElement ">" (elemName, attributes) - for node in nodes do buildNode isHtml sb node - do sb += "" + let inline buildParentNode (elemName, attributes : XmlAttribute array) (nodes : XmlNode list) = + do buildElement ">" (elemName, attributes) + for node in nodes do buildNode isHtml sb node + do sb += "" - match node with - | Text text -> do sb +! text - | ParentNode (e, nodes) -> do buildParentNode e nodes - | VoidElement e -> do buildElement (selfClosingBracket isHtml) e + match node with + | Text text -> do sb +! text + | ParentNode (e, nodes) -> do buildParentNode e nodes + | VoidElement e -> do buildElement (selfClosingBracket isHtml) e -let buildXmlNode = buildNode false -let buildHtmlNode = buildNode true + let buildXmlNode = buildNode false + let buildHtmlNode = buildNode true -let buildXmlNodes sb (nodes : XmlNode list) = for n in nodes do buildXmlNode sb n -let buildHtmlNodes sb (nodes : XmlNode list) = for n in nodes do buildHtmlNode sb n + let buildXmlNodes sb (nodes : XmlNode list) = for n in nodes do buildXmlNode sb n + let buildHtmlNodes sb (nodes : XmlNode list) = for n in nodes do buildHtmlNode sb n -let buildHtmlDocument sb (document : XmlNode) = - sb += "" +! Environment.NewLine - buildHtmlNode sb document + let buildHtmlDocument sb (document : XmlNode) = + sb += "" +! Environment.NewLine + buildHtmlNode sb document // --------------------------- // Render HTML/XML views // --------------------------- let renderXmlNode (node : XmlNode) : string = - let sb = new StringBuilder() in buildXmlNode sb node + let sb = new StringBuilder() in ViewBuilder.buildXmlNode sb node sb.ToString() let renderXmlNodes (nodes : XmlNode list) : string = - let sb = new StringBuilder() in buildXmlNodes sb nodes + let sb = new StringBuilder() in ViewBuilder.buildXmlNodes sb nodes sb.ToString() let renderHtmlNode (node : XmlNode) : string = - let sb = new StringBuilder() in buildHtmlNode sb node + let sb = new StringBuilder() in ViewBuilder.buildHtmlNode sb node sb.ToString() let renderHtmlNodes (nodes : XmlNode list) : string = - let sb = new StringBuilder() in buildHtmlNodes sb nodes + let sb = new StringBuilder() in ViewBuilder.buildHtmlNodes sb nodes sb.ToString() let renderHtmlDocument (document : XmlNode) : string = - let sb = new StringBuilder() in buildHtmlDocument sb document + let sb = new StringBuilder() in ViewBuilder.buildHtmlDocument sb document sb.ToString() \ No newline at end of file diff --git a/src/Giraffe/Middleware.fs b/src/Giraffe/Middleware.fs index 5b3d1e2e..56237b31 100644 --- a/src/Giraffe/Middleware.fs +++ b/src/Giraffe/Middleware.fs @@ -10,7 +10,6 @@ open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.DependencyInjection.Extensions open FSharp.Control.Tasks.V2.ContextInsensitive open Giraffe.Serialization -open Giraffe.StringBuilders // --------------------------- // Default middleware @@ -123,7 +122,6 @@ type IServiceCollection with /// Returns an `IServiceCollection` builder object. /// member this.AddGiraffe() = - this.TryAddSingleton() this.TryAddSingleton(NewtonsoftJsonSerializer(NewtonsoftJsonSerializer.DefaultSettings)) this.TryAddSingleton(DefaultXmlSerializer(DefaultXmlSerializer.DefaultSettings)) this.TryAddSingleton() diff --git a/src/Giraffe/ResponseWriters.fs b/src/Giraffe/ResponseWriters.fs index 55210aac..7091399b 100644 --- a/src/Giraffe/ResponseWriters.fs +++ b/src/Giraffe/ResponseWriters.fs @@ -9,7 +9,6 @@ open Microsoft.AspNetCore.Http open Microsoft.Net.Http.Headers open FSharp.Control.Tasks.V2.ContextInsensitive open Giraffe.GiraffeViewEngine -open Giraffe.StringBuilders // --------------------------- // HttpContext extensions @@ -170,17 +169,15 @@ type HttpContext with /// Task of `Some HttpContext` after writing to the body of the response. /// member this.WriteHtmlViewAsync (htmlView : XmlNode) = - let bytes = - use sbProvider = this.GetService() - let sb = sbProvider.Get() - buildHtmlDocument sb htmlView |> ignore - let chars = ArrayPool.Shared.Rent(sb.Length) - sb.CopyTo(0, chars, 0, sb.Length) - let result = Encoding.UTF8.GetBytes(chars, 0, sb.Length) - ArrayPool.Shared.Return chars - result + let sb = new StringBuilder() + ViewBuilder.buildHtmlDocument sb htmlView |> ignore + let chars = ArrayPool.Shared.Rent(sb.Length) + sb.CopyTo(0, chars, 0, sb.Length) + let result = Encoding.UTF8.GetBytes(chars, 0, sb.Length) + ArrayPool.Shared.Return chars + this.SetContentType "text/html" - this.WriteBytesAsync bytes + this.WriteBytesAsync result // --------------------------- // HttpHandler functions diff --git a/src/Giraffe/StringBuilderProviders.fs b/src/Giraffe/StringBuilderProviders.fs deleted file mode 100644 index 9fcbe489..00000000 --- a/src/Giraffe/StringBuilderProviders.fs +++ /dev/null @@ -1,50 +0,0 @@ -namespace Giraffe.StringBuilders - -open System -open System.Text - -type private StringBuilderCache = - - [] - [] - static val mutable private sb : StringBuilder - - [] - [] - static val mutable private inUse : bool - - static member Get (capacity : int) (maxCapacity : int) : StringBuilder = - match StringBuilderCache.inUse with - | true -> new StringBuilder(capacity) - | false -> - StringBuilderCache.inUse <- true - - let sb = StringBuilderCache.sb - - match sb <> null && sb.Capacity >= capacity with - | true -> sb.Clear() - | false -> - let sb' = new StringBuilder(capacity) - if capacity <= maxCapacity then - StringBuilderCache.sb <- sb' - sb' - - static member Release() : unit = - StringBuilderCache.inUse <- false - -[] -module StringBuilderProvider = - - type IStringBuilderProvider = - inherit IDisposable - abstract member Get : unit -> StringBuilder - - type DefaultStringBuilderProvider() = - interface IStringBuilderProvider with - member __.Get() = new StringBuilder() - member __.Dispose() = () - - type ThreadStaticStringBuilderCache (defaultCapacity, maxCapacity) = - interface IStringBuilderProvider with - member __.Get() = StringBuilderCache.Get defaultCapacity maxCapacity - member __.Dispose() = StringBuilderCache.Release() \ No newline at end of file diff --git a/tests/Giraffe.Tests/Helpers.fs b/tests/Giraffe.Tests/Helpers.fs index b8639175..4ede18a4 100644 --- a/tests/Giraffe.Tests/Helpers.fs +++ b/tests/Giraffe.Tests/Helpers.fs @@ -19,7 +19,6 @@ open NSubstitute open Newtonsoft.Json open Giraffe open Giraffe.Serialization -open Giraffe.StringBuilders // --------------------------------- // Common functions @@ -98,12 +97,6 @@ let createHost (configureApp : 'Tuple -> IApplicationBuilder -> unit) .Configure(Action (configureApp args)) .ConfigureServices(Action configureServices) -let mockCache (ctx : HttpContext) = - ctx.RequestServices - .GetService(typeof) - .Returns(new DefaultStringBuilderProvider()) - |> ignore - let mockJson (ctx : HttpContext) (settings : JsonSerializerSettings option) = let jsonSettings = defaultArg settings NewtonsoftJsonSerializer.DefaultSettings diff --git a/tests/Giraffe.Tests/HttpContextExtensionsTests.fs b/tests/Giraffe.Tests/HttpContextExtensionsTests.fs index bdb65da2..4619a0fc 100644 --- a/tests/Giraffe.Tests/HttpContextExtensionsTests.fs +++ b/tests/Giraffe.Tests/HttpContextExtensionsTests.fs @@ -78,7 +78,6 @@ let ``TryGetQueryStringValue during HTTP GET request with query string returns c [] let ``WriteHtmlViewAsync should add html to the context`` () = let ctx = Substitute.For() - mockCache ctx let testHandler = fun (next : HttpFunc) (ctx : HttpContext) -> diff --git a/tests/Giraffe.Tests/HttpHandlerTests.fs b/tests/Giraffe.Tests/HttpHandlerTests.fs index 2f9c5476..87758d1b 100644 --- a/tests/Giraffe.Tests/HttpHandlerTests.fs +++ b/tests/Giraffe.Tests/HttpHandlerTests.fs @@ -310,7 +310,6 @@ let ``POST "/either" with unsupported Accept header returns 404 "Not found"`` () [] let ``GET "/person" returns rendered HTML view`` () = let ctx = Substitute.For() - mockCache ctx let personView model = html [] [ From 9abc918f1efc55aec07e6441cb2f6ff22a0aca9a Mon Sep 17 00:00:00 2001 From: Dmitry Kushnir Date: Thu, 13 Sep 2018 17:29:59 +0300 Subject: [PATCH 16/43] fixed benchmark build --- tests/Giraffe.Benchmarks/Program.fs | 31 +++++++++++++---------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/tests/Giraffe.Benchmarks/Program.fs b/tests/Giraffe.Benchmarks/Program.fs index 5a9a0dc4..a79bc9ac 100644 --- a/tests/Giraffe.Benchmarks/Program.fs +++ b/tests/Giraffe.Benchmarks/Program.fs @@ -3,13 +3,10 @@ open BenchmarkDotNet.Running; open Giraffe.GiraffeViewEngine open System.Text open System.Buffers -open Giraffe.Serialization.Cache [] type HtmlUtf8Benchmark() = - let cache = new ThreadStaticStringBuilderCache(1024, 32 * 1024) :> IStringBuilderCache - let doc = div [] [ div [ _class "top-bar" ] @@ -48,30 +45,30 @@ type HtmlUtf8Benchmark() = [ rawText "Search" ] ] ] ] ] ] + let stringBuilder = new StringBuilder(16 * 1024) + [] - member this.String() = + member this.Default() = renderHtmlDocument doc |> Encoding.UTF8.GetBytes [] - member this.Cached() = - let sb = cache.Get() - renderHtmlDocument' sb doc - sb.ToString() |> Encoding.UTF8.GetBytes |> ignore - cache.Release sb + member this.CachedStringBuilder() = + ViewBuilder.buildHtmlDocument stringBuilder doc + stringBuilder.ToString() |> Encoding.UTF8.GetBytes |> ignore + stringBuilder.Clear(); [] - member this.CachedAndPooled() = - let sb = cache.Get() - renderHtmlDocument' sb doc - let chars = ArrayPool.Shared.Rent(sb.Length) - sb.CopyTo(0, chars, 0, sb.Length) - Encoding.UTF8.GetBytes(chars, 0, sb.Length) |> ignore + member this.CachedStringBuilderPooledUtf8Array() = + ViewBuilder.buildHtmlDocument stringBuilder doc + let chars = ArrayPool.Shared.Rent(stringBuilder.Length) + stringBuilder.CopyTo(0, chars, 0, stringBuilder.Length) + Encoding.UTF8.GetBytes(chars, 0, stringBuilder.Length) |> ignore ArrayPool.Shared.Return(chars) - cache.Release sb + stringBuilder.Clear() + [] let main args = let asm = typeof.Assembly BenchmarkSwitcher.FromAssembly(asm).Run(args) |> ignore 0 - From 6414108f42c7431cae954ea2a2c282704b02ddd9 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Thu, 13 Sep 2018 19:43:54 +0100 Subject: [PATCH 17/43] Removed redundant `task {}` CE override Also updated the docs to explicitly mention that the FSharp.Controls.Tasks.V2 namespace has to be opened. Fixes #298 --- DOCUMENTATION.md | 5 +++-- src/Giraffe/Common.fs | 11 +---------- src/Giraffe/ComputationExpressions.fs | 2 +- src/Giraffe/FormatExpressions.fs | 1 - src/Giraffe/ResponseWriters.fs | 1 - 5 files changed, 5 insertions(+), 15 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 70cdf8b1..eba26f08 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -153,9 +153,10 @@ let webApp = Another important aspect of Giraffe is that it natively works with .NET's `Task` and `Task<'T>` objects instead of relying on F#'s `async {}` workflows. The main benefit of this is that it removes the necessity of converting back and forth between tasks and async workflows when building a Giraffe web application (because ASP.NET Core only works with tasks out of the box). -For this purpose Giraffe uses the `task {}` computation expression which comes with the [`TaskBuilder.fs` NuGet package](https://www.nuget.org/packages/TaskBuilder.fs/). Syntactically it works identical to F#'s async workflows: +For this purpose Giraffe uses the `task {}` computation expression which comes with the [`TaskBuilder.fs` NuGet package](https://www.nuget.org/packages/TaskBuilder.fs/). Syntactically it works identical to F#'s async workflows (after opening the `FSharp.Control.Tasks.V2.ContextInsensitive` module): ```fsharp +open FSharp.Control.Tasks.V2.ContextInsensitive open Giraffe let personHandler = @@ -166,7 +167,7 @@ let personHandler = } ``` -The `task {}` CE is an independent project maintained by [Robert Peele](https://github.com/rspeele) and can be used from any other F# application as well. All you have to do is add a reference to the `TaskBuilder.fs` NuGet library and open the module: +The `task {}` CE is an independent project maintained by [Robert Peele](https://github.com/rspeele) and can be used from any other F# application as well. All you have to do is add a reference to the `TaskBuilder.fs` NuGet library and open the `FSharp.Control.Tasks.V2` module: ```fsharp open FSharp.Control.Tasks.V2 diff --git a/src/Giraffe/Common.fs b/src/Giraffe/Common.fs index 1fe36820..ee70dd87 100644 --- a/src/Giraffe/Common.fs +++ b/src/Giraffe/Common.fs @@ -3,16 +3,7 @@ module Giraffe.Common open System open System.IO - -// --------------------------- -// Override the default task CE -// --------------------------- - -/// **Context insensitive Task CE** -/// -/// All tasks are configured with `ConfigurAwait(false)`. -/// -let task = FSharp.Control.Tasks.V2.ContextInsensitive.task +open FSharp.Control.Tasks.V2.ContextInsensitive // --------------------------- // Useful extension methods diff --git a/src/Giraffe/ComputationExpressions.fs b/src/Giraffe/ComputationExpressions.fs index 33f28f01..a9bc2b8c 100644 --- a/src/Giraffe/ComputationExpressions.fs +++ b/src/Giraffe/ComputationExpressions.fs @@ -27,4 +27,4 @@ type ResultBuilder() = /// /// Enables control flow and binding of `Result<'T, 'TError>` objects /// -let res = ResultBuilder() +let res = ResultBuilder() \ No newline at end of file diff --git a/src/Giraffe/FormatExpressions.fs b/src/Giraffe/FormatExpressions.fs index 2e60bd69..39a7685b 100644 --- a/src/Giraffe/FormatExpressions.fs +++ b/src/Giraffe/FormatExpressions.fs @@ -1,7 +1,6 @@ module Giraffe.FormatExpressions open System -open System.Net open System.Text.RegularExpressions open FSharp.Core open Microsoft.FSharp.Reflection diff --git a/src/Giraffe/ResponseWriters.fs b/src/Giraffe/ResponseWriters.fs index 7091399b..0f6e6d40 100644 --- a/src/Giraffe/ResponseWriters.fs +++ b/src/Giraffe/ResponseWriters.fs @@ -1,7 +1,6 @@ [] module Giraffe.ResponseWriters -open System open System.IO open System.Text open System.Buffers From 05b89281c9bc4fa5567f30ab2f5d3f1fef78b1b7 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Thu, 13 Sep 2018 19:47:44 +0100 Subject: [PATCH 18/43] Updated next release notes Fixes #298 --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 3b5cbe9c..18fe81c4 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -9,6 +9,8 @@ Release Notes This change should not affect the majority of Giraffe users unless you were constructing your own `XmlNode` elements which were of type `RawText` or `EncodedText`. +- Removed the `task {}` override in Giraffe which was forcing the `FSharp.Control.Tasks.V2.ContextInsensitive` version of the Task CE. This change has no effect on existing Giraffe applications other than that it might require an additional open statement of the aforementioned module in some places, but behaviour wise it will remain exactly the same (even if the context sensitive module gets opened, because there is no difference between context sensitive or insensitive in the context of an ASP.NET Core application). + #### Improvements - Huge performance gains by changing the underlying way of how views are being composed by the `GriaffeViewEngine`. From 98c9cb7df73754d742b1807de1b3c56754c6bb4c Mon Sep 17 00:00:00 2001 From: Dmitry Kushnir Date: Fri, 14 Sep 2018 01:50:09 +0300 Subject: [PATCH 19/43] extended json serializer contract --- src/Giraffe/Serialization.fs | 42 +++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/Giraffe/Serialization.fs b/src/Giraffe/Serialization.fs index 9703cc23..f8fac9c8 100644 --- a/src/Giraffe/Serialization.fs +++ b/src/Giraffe/Serialization.fs @@ -10,7 +10,7 @@ module Json = open System.Threading.Tasks open Newtonsoft.Json open Newtonsoft.Json.Serialization - open FSharp.Control.Tasks.V2.ContextInsensitive + open System.Text /// **Description** /// @@ -18,10 +18,12 @@ module Json = /// [] type IJsonSerializer = - abstract member Serialize : obj -> string - abstract member Deserialize<'T> : string -> 'T - abstract member Deserialize<'T> : Stream -> 'T - abstract member DeserializeAsync<'T> : Stream -> Task<'T> + abstract member Serialize<'T> : 'T -> string + abstract member SerializeToBytes<'T> : 'T -> byte[] + abstract member SerializeAsync<'T> : 'T * Stream -> Task + abstract member Deserialize<'T> : string -> 'T + abstract member Deserialize<'T> : Stream -> 'T + abstract member DeserializeAsync<'T> : Stream -> Task<'T> /// **Description** /// @@ -30,14 +32,30 @@ module Json = /// Serializes objects to camel cased JSON code. /// type NewtonsoftJsonSerializer (settings : JsonSerializerSettings) = + + let Utf8EncodingWithoutBom = new UTF8Encoding(false) + let DefaultBufferSize = 1024 + static member DefaultSettings = JsonSerializerSettings( ContractResolver = CamelCasePropertyNamesContractResolver()) interface IJsonSerializer with - member __.Serialize (o : obj) = JsonConvert.SerializeObject(o, settings) + member __.Serialize (o : 'T) = JsonConvert.SerializeObject(o, settings) + + member __.SerializeToBytes (o: 'T) = + let json = JsonConvert.SerializeObject(o, settings) + Encoding.UTF8.GetBytes(json) + + member __.SerializeAsync (o: 'T, stream: Stream) = + use sw = new StreamWriter(stream, Utf8EncodingWithoutBom, DefaultBufferSize, true) + use jw = new JsonTextWriter(sw) + let sr = JsonSerializer.Create settings + sr.Serialize(jw, o) + Task.CompletedTask - member __.Deserialize<'T> (json : string) = JsonConvert.DeserializeObject<'T>(json, settings) + member __.Deserialize<'T> (json : string) = + JsonConvert.DeserializeObject<'T>(json, settings) member __.Deserialize<'T> (stream : Stream) = use sr = new StreamReader(stream, true) @@ -46,12 +64,10 @@ module Json = sr.Deserialize<'T> jr member __.DeserializeAsync<'T> (stream : Stream) = - task { - use sr = new StreamReader(stream, true) - use jr = new JsonTextReader(sr) - let sr = JsonSerializer.Create settings - return sr.Deserialize<'T> jr - } + use sr = new StreamReader(stream, true) + use jr = new JsonTextReader(sr) + let sr = JsonSerializer.Create settings + Task.FromResult( sr.Deserialize<'T> jr ) // --------------------------- // XML From 843abf42a11ec0735d6eddd7eb5eaeef4afe6753 Mon Sep 17 00:00:00 2001 From: Dmitry Kushnir Date: Fri, 14 Sep 2018 02:18:17 +0300 Subject: [PATCH 20/43] response writers --- src/Giraffe/ResponseWriters.fs | 51 ++++++++++++++-- tests/Giraffe.Tests/HttpHandlerTests.fs | 79 +++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 4 deletions(-) diff --git a/src/Giraffe/ResponseWriters.fs b/src/Giraffe/ResponseWriters.fs index 0f6e6d40..cd1cf12e 100644 --- a/src/Giraffe/ResponseWriters.fs +++ b/src/Giraffe/ResponseWriters.fs @@ -80,11 +80,34 @@ type HttpContext with /// /// Task of `Some HttpContext` after writing to the body of the response. /// - member this.WriteJsonAsync (dataObj : obj) = + member this.WriteJsonAsync<'T> (dataObj : 'T) = this.SetContentType "application/json" let serializer = this.GetJsonSerializer() - serializer.Serialize dataObj - |> this.WriteStringAsync + serializer.SerializeToBytes dataObj + |> this.WriteBytesAsync + + /// **Description** + /// + /// Serializes an object to JSON and writes the output to the body of the HTTP response using chunked transfer encoding. + /// + /// It also sets the HTTP `Content-Type` header to `application/json` and sets the `Content-Length` header accordingly. + /// + /// The JSON serializer can be configured in the ASP.NET Core startup code by registering a custom class of type `IJsonSerializer`. + /// + /// **Parameters** + /// + /// - `dataObj`: The object to be send back to the client. + /// + /// **Output** + /// + /// Task of `Some HttpContext` after writing to the body of the response. + /// + member this.WriteJsonChunkedAsync<'T> (dataObj : 'T) = task { + this.SetContentType "application/json" + let serializer = this.GetJsonSerializer() + do! serializer.SerializeAsync (dataObj, this.Response.Body) + return Some this + } /// **Description** /// @@ -246,10 +269,30 @@ let text (str : string) : HttpHandler = /// /// A Giraffe `HttpHandler` function which can be composed into a bigger web application. /// -let json (dataObj : obj) : HttpHandler = +let json<'T> (dataObj : 'T) : HttpHandler = fun (next : HttpFunc) (ctx : HttpContext) -> ctx.WriteJsonAsync dataObj +/// **Description** +/// +/// Serializes an object to JSON and writes the output to the body of the HTTP response using chunked transfer encoding. +/// +/// It also sets the HTTP `Content-Type` header to `application/json` and sets the `Content-Length` header accordingly. +/// +/// The JSON serializer can be configured in the ASP.NET Core startup code by registering a custom class of type `IJsonSerializer`. +/// +/// **Parameters** +/// +/// - `dataObj`: The object to be send back to the client. +/// +/// **Output** +/// +/// A Giraffe `HttpHandler` function which can be composed into a bigger web application. +/// +let jsonChunked<'T> (dataObj : 'T) : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + ctx.WriteJsonChunkedAsync dataObj + /// **Description** /// /// Serializes an object to XML and writes the output to the body of the HTTP response. diff --git a/tests/Giraffe.Tests/HttpHandlerTests.fs b/tests/Giraffe.Tests/HttpHandlerTests.fs index 87758d1b..9b24e519 100644 --- a/tests/Giraffe.Tests/HttpHandlerTests.fs +++ b/tests/Giraffe.Tests/HttpHandlerTests.fs @@ -93,6 +93,85 @@ let ``GET "/json" with custom json settings returns json object`` () = | Some ctx -> Assert.Equal(expected, getBody ctx) } +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +let ``GET "/jsonChunked" returns json object`` (size: int) = + let ctx = Substitute.For() + mockJson ctx None + let app = + GET >=> choose [ + route "/" >=> text "Hello World" + route "/foo" >=> text "bar" + route "/jsonChunked" >=> json ( Array.replicate size { Foo = "john"; Bar = "doe"; Age = 30 } ) + setStatusCode 404 >=> text "Not found" ] + + ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore + ctx.Request.Path.ReturnsForAnyArgs (PathString("/jsonChunked")) |> ignore + ctx.Response.Body <- new MemoryStream() + + let expected = + let o = "{\"foo\":\"john\",\"bar\":\"doe\",\"age\":30}" + let os = Array.replicate size o |> String.concat "," + "[" + os + "]" + + task { + let! result = app next ctx + + match result with + | None -> assertFailf "Result was expected to be %s" expected + | Some ctx -> Assert.Equal(expected, getBody ctx) + } + +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +let ``GET "/jsonChunked" with custom json settings returns json object`` (size: int) = + let settings = Newtonsoft.Json.JsonSerializerSettings() + let ctx = Substitute.For() + mockJson ctx (Some settings) + let app = + GET >=> choose [ + route "/" >=> text "Hello World" + route "/foo" >=> text "bar" + route "/jsonChunked" >=> json ( Array.replicate size { Foo = "john"; Bar = "doe"; Age = 30 } ) + setStatusCode 404 >=> text "Not found" ] + + ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore + ctx.Request.Path.ReturnsForAnyArgs (PathString("/jsonChunked")) |> ignore + ctx.Response.Body <- new MemoryStream() + + let expected = + let o = "{\"Foo\":\"john\",\"Bar\":\"doe\",\"Age\":30}" + let os = Array.replicate size o |> String.concat "," + "[" + os + "]" + + task { + let! result = app next ctx + + match result with + | None -> assertFailf "Result was expected to be %s" expected + | Some ctx -> Assert.Equal(expected, getBody ctx) + } + [] let ``POST "/post/1" returns "1"`` () = let ctx = Substitute.For() From 911276555477b7cab2e4fe9a8e1105e32f96ec61 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 14 Sep 2018 10:18:42 -0700 Subject: [PATCH 21/43] enable SourceLink --- src/Directory.Build.props | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/Directory.Build.props diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 00000000..99e65b6a --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,11 @@ + + + + true + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + + \ No newline at end of file From d8b6b3a87e3f5b9137f972f50caa0cbb9709e9ee Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 14 Sep 2018 11:54:35 -0700 Subject: [PATCH 22/43] .NET Core SDK 2.1.401 --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index b11d921c..e27592b0 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "projects": [ "src", "tests" ], "sdk": { - "version": "2.1.400" + "version": "2.1.401" } } \ No newline at end of file From e3731de12efa085ef98a6788422c4404e6f85d97 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 14 Sep 2018 12:58:35 -0700 Subject: [PATCH 23/43] .NET Core SDK 2.1.402 --- .travis.yml | 2 +- global.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 852d84b7..d5393be6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: csharp sudo: required dist: trusty -dotnet: 2.1.400 +dotnet: 2.1.402 mono: - 4.6.1 - 4.8.1 diff --git a/global.json b/global.json index e27592b0..a24bb130 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "projects": [ "src", "tests" ], "sdk": { - "version": "2.1.401" + "version": "2.1.402" } } \ No newline at end of file From 1bded570f46381a9813e780c34b63130e12bf7de Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 14 Sep 2018 14:13:09 -0700 Subject: [PATCH 24/43] FS2003 --- src/Directory.Build.props | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 99e65b6a..b5fa4167 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,6 +4,8 @@ true true $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + FS2003 From 08bd0cbc802f1d9eeae460126ab481ac9783f9f8 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 14 Sep 2018 14:22:35 -0700 Subject: [PATCH 25/43] don't change sdk version --- .travis.yml | 2 +- global.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index d5393be6..852d84b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: csharp sudo: required dist: trusty -dotnet: 2.1.402 +dotnet: 2.1.400 mono: - 4.6.1 - 4.8.1 diff --git a/global.json b/global.json index a24bb130..b11d921c 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "projects": [ "src", "tests" ], "sdk": { - "version": "2.1.402" + "version": "2.1.400" } } \ No newline at end of file From 458d256e8bbdf7b16ae65d4d8cba6771ca6a6429 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 14 Sep 2018 14:44:26 -0700 Subject: [PATCH 26/43] Microsoft.NET.Test.Sdk does not need Program.fs anymore --- samples/SampleApp/SampleApp.Tests/Program.fs | 2 -- samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj | 1 - 2 files changed, 3 deletions(-) delete mode 100644 samples/SampleApp/SampleApp.Tests/Program.fs diff --git a/samples/SampleApp/SampleApp.Tests/Program.fs b/samples/SampleApp/SampleApp.Tests/Program.fs deleted file mode 100644 index 75f178df..00000000 --- a/samples/SampleApp/SampleApp.Tests/Program.fs +++ /dev/null @@ -1,2 +0,0 @@ -module Program -let [] main _ = 0 \ No newline at end of file diff --git a/samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj b/samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj index c8251c6b..fa3ba983 100644 --- a/samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj +++ b/samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj @@ -20,7 +20,6 @@ - \ No newline at end of file From ac0e9f85cd4abc83b924aee54be1c29ecb246440 Mon Sep 17 00:00:00 2001 From: Cameron Taggart Date: Fri, 14 Sep 2018 15:01:37 -0700 Subject: [PATCH 27/43] use newest sdk 2.1.402 --- .travis.yml | 2 +- global.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 852d84b7..d5393be6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: csharp sudo: required dist: trusty -dotnet: 2.1.400 +dotnet: 2.1.402 mono: - 4.6.1 - 4.8.1 diff --git a/global.json b/global.json index b11d921c..a24bb130 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "projects": [ "src", "tests" ], "sdk": { - "version": "2.1.400" + "version": "2.1.402" } } \ No newline at end of file From b397803ca74435ebb8590662a83250d283fa4ccc Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Sat, 15 Sep 2018 11:18:35 +0100 Subject: [PATCH 28/43] Added support for Short GUIDs and Short IDs Fixes #282 --- DOCUMENTATION.md | 64 +++++++- RELEASE_NOTES.md | 6 +- global.json | 2 +- src/Giraffe/Common.fs | 142 +++++++++++++++++- src/Giraffe/FormatExpressions.fs | 15 +- .../Giraffe.Benchmarks.fsproj | 2 +- tests/Giraffe.Benchmarks/Program.fs | 12 +- tests/Giraffe.Tests/FormatExpressionTests.fs | 14 ++ tests/Giraffe.Tests/Giraffe.Tests.fsproj | 1 + tests/Giraffe.Tests/GuidAndIdTests.fs | 113 ++++++++++++++ tests/Giraffe.Tests/RoutingTests.fs | 27 +++- 11 files changed, 383 insertions(+), 15 deletions(-) create mode 100644 tests/Giraffe.Tests/GuidAndIdTests.fs diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index eba26f08..ac32beaf 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -45,6 +45,7 @@ An in depth functional reference to all of Giraffe's default features. - [JSON](#json) - [XML](#xml) - [Miscellaneous](#miscellaneous) + - [Short GUIDs and Short IDs](#short-guids-and-short-ids) - [Common Helper Functions](#common-helper-functions) - [Computation Expressions](#computation-expressions) - [Additional Features](#additional-features) @@ -896,7 +897,24 @@ The format string supports the following format chars: | `%i` | `int` | | `%d` | `int64` | | `%f` | `float`/`double` | -| `%O` | `Guid` | +| `%O` | `Guid` (including short GUIDs*) | +| `%u` | `uint64` (formatted as a short ID*) | + +*) Please note that the `%O` and `%u` format characters also support URL friendly short GUIDs and IDs. + +The `%O` format character supports GUIDs in the format of: + +- `00000000000000000000000000000000` +- `00000000-0000-0000-0000-000000000000` +- `Xy0MVKupFES9NpmZ9TiHcw` + +The last string represents an example of a [Short GUID](https://madskristensen.net/blog/A-shorter-and-URL-friendly-GUID) which is a normal GUID shortened into a URL encoded 22 character long string. Routes which use the `%O` format character will be able to automatically resolve a [Short GUID](https://madskristensen.net/blog/A-shorter-and-URL-friendly-GUID) as well as a normal GUID into a `System.Guid` argument. + +The `%u` format character can only resolve an 11 character long [Short ID](https://webapps.stackexchange.com/questions/54443/format-for-id-of-youtube-video) (aka YouTube ID) into a `uint64` value. + +Short GUIDs and short IDs are popular choices to make URLs shorter and friendlier whilst still mapping to a unique `System.Guid` or `uint64` value on the server side. + +[Short GUIDs and IDs can also be resolved from query string parameters](#short-guids-and-short-ids) by making use of the `ShortGuid` and `ShortId` helper modules. #### routeCif @@ -912,6 +930,8 @@ let webApp = ] ``` +Please be aware that a case insensitive URL matching will return unexpected results in combination with case sensitive arguments such as short GUIDs and short IDs. + #### routeBind If you need to bind route parameters directly to a type then you can use the `routeBind<'T>` http handler. Unlike `routef` or `routeCif` which work with a format string the `routeBind<'T>` http handler tries to match named parameters to the properties of a given type `'T`: @@ -3082,6 +3102,48 @@ let customHandler (dataObj : obj) : HttpHandler = On top of default HTTP related functions such as `HttpContext` extension methods and `HttpHandler` functions Giraffe also provides a few other helper functions which are commonly required in Giraffe web applications. +### Short GUIDs and Short IDs + +The `ShortGuid` and `ShortId` modules offer helper functions to work with [Short GUIDs](https://madskristensen.net/blog/A-shorter-and-URL-friendly-GUID) and [Short IDs](https://webapps.stackexchange.com/questions/54443/format-for-id-of-youtube-video) inside Giraffe. + +#### ShortGuid + +The `ShortGuid.fromGuid` function will convert a `System.Guid` into a URL friendly 22 character long `string` value. + +The `ShortGuid.toGuid` function will convert a 22 character short GUID `string` into a valid `System.Guid` object. This function can be useful when converting a `string` query parameter into a valid `Guid` argument: + +```fsharp +let someHttpHandler : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + let guid = + match ctx.TryGetQueryStringValue "id" with + | None -> Guid.Empty + | Some shortGuid -> ShortGuid.toGuid shortGuid + + // Do something with `guid`... + // Return a Task +``` + +#### ShortId + +The `ShortId.fromUInt64` function will convert an `uint64` into a URL friendly 11 character long `string` value. + +The `ShortId.toUInt64` function will convert a 11 character short ID `string` into a `uint64` value. This function can be useful when converting a `string` query parameter into a valid `uint64` argument: + +```fsharp +let someHttpHandler : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + let id = + match ctx.TryGetQueryStringValue "id" with + | None -> 0UL + | Some shortId -> ShortId.toUInt64 shortId + + // Do something with `id`... + // Return a Task +``` + +Short GUIDs and short IDs can also be [automatically resolved from route arguments](#routef). + ### Common Helper Functions #### DateTime Extension methods diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 18fe81c4..dbd1749f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -13,7 +13,11 @@ This change should not affect the majority of Giraffe users unless you were cons #### Improvements -- Huge performance gains by changing the underlying way of how views are being composed by the `GriaffeViewEngine`. +- Significant performance gains by changing the underlying way of how views are being composed by the `GriaffeViewEngine`. + +#### New featurs + +- Support for short GUIDs and short IDs (aka YouTube IDs) [in route arguments](https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#routef) and [query string parameters](https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#short-guids-and-short-ids). ## 2.0.1 diff --git a/global.json b/global.json index e27592b0..a24bb130 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "projects": [ "src", "tests" ], "sdk": { - "version": "2.1.401" + "version": "2.1.402" } } \ No newline at end of file diff --git a/src/Giraffe/Common.fs b/src/Giraffe/Common.fs index ee70dd87..801edc09 100644 --- a/src/Giraffe/Common.fs +++ b/src/Giraffe/Common.fs @@ -88,4 +88,144 @@ let readFileAsStringAsync (filePath : string) = task { use reader = new StreamReader(filePath) return! reader.ReadToEndAsync() - } \ No newline at end of file + } + +// --------------------------- +// Short GUIDs and IDs +// --------------------------- + +/// **Description** +/// +/// Short GUIDs are a shorter, URL-friendlier version +/// of the traditional `System.Guid` type. +/// +/// Short GUIDs are always 22 characters long, which let's +/// one save a total of 10 characters in comparison to using +/// a normal `System.Guid` as identifier. +/// +/// Additionally a Short GUID is by default a URL encoded +/// string which doesn't need extra character replacing +/// before using of it in a URL query parameter. +/// +/// All Short GUID strings map directly to a `System.Guid` +/// objet and the `ShortGuid` module can be used to convert +/// a `System.Guid` into a short GUID `string` and vice versa. +/// +/// For more information please check: +/// https://madskristensen.net/blog/A-shorter-and-URL-friendly-GUID +/// +[] +module ShortGuid = + + /// **Description** + /// + /// Converts a `System.Guid` into a 22 character long + /// short GUID string. + /// + /// **Parameters** + /// + /// - `guid`: The `System.Guid` to be converted into a short GUID. + /// + /// **Output** + /// + /// Returns a 22 character long URL encoded short GUID string. + /// + let fromGuid (guid : Guid) = + guid.ToByteArray() + |> Convert.ToBase64String + |> (fun str -> + str.Replace("/", "_") + .Replace("+", "-") + .Substring(0, 22)) + + /// **Description** + /// + /// Converts a 22 character short GUID string into the matching `System.Guid`. + /// + /// **Parameters** + /// + /// - `shortGuid`: The short GUID string to be converted into a `System.Guid`. + /// + /// **Output** + /// + /// Returns a `System.Guid` object. + /// + let toGuid (shortGuid : string) = + shortGuid.Replace("_", "/") + .Replace("-", "+") + |> (fun str -> str + "==") + |> Convert.FromBase64String + |> Guid + +/// **Description** +/// +/// Short IDs are a shorter, URL-friendlier version +/// of an unisgned 64-bit integer value (`uint64` in F# and `ulong` in C#). +/// +/// Short IDs are always 11 characters long, which let's +/// one save a total of 9 characters in comparison to using +/// a normal `uint64` value as identifier. +/// +/// Additionally a Short ID is by default a URL encoded +/// string which doesn't need extra character replacing +/// before using it in a URL query parameter. +/// +/// All Short ID strings map directly to a `uint64` object +/// and the `ShortId` module can be used to convert an +/// `unint64` value into a short ID `string` and vice versa. +/// +/// For more information please check: +/// https://webapps.stackexchange.com/questions/54443/format-for-id-of-youtube-video +/// +[] +module ShortId = + + /// **Description** + /// + /// Converts a `uint64` value into a 11 character long + /// short ID string. + /// + /// **Parameters** + /// + /// - `id`: The `uint64` to be converted into a short ID. + /// + /// **Output** + /// + /// Returns a 11 character long URL encoded short ID string. + /// + let fromUInt64 (id : uint64) = + BitConverter.GetBytes id + |> (fun arr -> + match BitConverter.IsLittleEndian with + | true -> Array.Reverse arr; arr + | false -> arr) + |> Convert.ToBase64String + |> (fun str -> + str.Remove(11, 1) + .Replace("/", "_") + .Replace("+", "-")) + + /// **Description** + /// + /// Converts a 11 character short ID string into the matching `uint64` value. + /// + /// **Parameters** + /// + /// - `shortId`: The short ID string to be converted into a `uint64` value. + /// + /// **Output** + /// + /// Returns a `uint64` value. + /// + let toUInt64 (shortId : string) = + let bytes = + shortId.Replace("_", "/") + .Replace("-", "+") + |> (fun str -> str + "=") + |> Convert.FromBase64String + |> (fun arr -> + match BitConverter.IsLittleEndian with + | true -> Array.Reverse arr; arr + | false -> arr) + BitConverter.ToUInt64 (bytes, 0) + diff --git a/src/Giraffe/FormatExpressions.fs b/src/Giraffe/FormatExpressions.fs index 39a7685b..26caad55 100644 --- a/src/Giraffe/FormatExpressions.fs +++ b/src/Giraffe/FormatExpressions.fs @@ -22,8 +22,15 @@ let private formatStringMap = // https://github.com/aspnet/Mvc/issues/4599 str.Replace("%2F", "/").Replace("%2f", "/") - let guidFormatStr = - "([0-9A-Fa-f]{8}\-[0-9A-Fa-f]{4}\-[0-9A-Fa-f]{4}\-[0-9A-Fa-f]{4}\-[0-9A-Fa-f]{12}|[0-9A-Fa-f]{32})" + let parseGuid (str : string) = + match str.Length with + | 22 -> ShortGuid.toGuid str + | _ -> Guid str + + let guidPattern = + "([0-9A-Fa-f]{8}\-[0-9A-Fa-f]{4}\-[0-9A-Fa-f]{4}\-[0-9A-Fa-f]{4}\-[0-9A-Fa-f]{12}|[0-9A-Fa-f]{32}|[-_0-9A-Za-z]{22})" + + let shortIdPattern = "([-_0-9A-Za-z]{10}[048AEIMQUYcgkosw])" dict [ // Char Regex Parser @@ -34,7 +41,8 @@ let private formatStringMap = 'i', ("(-?\d+)", int32 >> box) // int 'd', ("(-?\d+)", int64 >> box) // int64 'f', ("(-?\d+\.{1}\d+)", float >> box) // float - 'O', (guidFormatStr, Guid >> box) // Guid + 'O', (guidPattern, parseGuid >> box) // Guid + 'u', (shortIdPattern, ShortId.toUInt64 >> box) // uint64 ] let private convertToRegexPatternAndFormatChars (formatString : string) = @@ -141,6 +149,7 @@ let validateFormat (format : PrintfFormat<_,_,_,_, 'T>) = 'd' , typeof // int64 'f' , typeof // float 'O' , typeof // guid + 'u' , typeof // guid ] let tuplePrint pos last name = diff --git a/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj b/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj index 35944153..82677e09 100644 --- a/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj +++ b/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj @@ -22,4 +22,4 @@ - + \ No newline at end of file diff --git a/tests/Giraffe.Benchmarks/Program.fs b/tests/Giraffe.Benchmarks/Program.fs index a79bc9ac..c2b47162 100644 --- a/tests/Giraffe.Benchmarks/Program.fs +++ b/tests/Giraffe.Benchmarks/Program.fs @@ -7,7 +7,7 @@ open System.Buffers [] type HtmlUtf8Benchmark() = - let doc = + let doc = div [] [ div [ _class "top-bar" ] [ div [ _class "top-bar-left" ] @@ -48,20 +48,20 @@ type HtmlUtf8Benchmark() = let stringBuilder = new StringBuilder(16 * 1024) [] - member this.Default() = + member this.Default() = renderHtmlDocument doc |> Encoding.UTF8.GetBytes [] - member this.CachedStringBuilder() = + member this.CachedStringBuilder() = ViewBuilder.buildHtmlDocument stringBuilder doc stringBuilder.ToString() |> Encoding.UTF8.GetBytes |> ignore stringBuilder.Clear(); [] - member this.CachedStringBuilderPooledUtf8Array() = + member this.CachedStringBuilderPooledUtf8Array() = ViewBuilder.buildHtmlDocument stringBuilder doc let chars = ArrayPool.Shared.Rent(stringBuilder.Length) - stringBuilder.CopyTo(0, chars, 0, stringBuilder.Length) + stringBuilder.CopyTo(0, chars, 0, stringBuilder.Length) Encoding.UTF8.GetBytes(chars, 0, stringBuilder.Length) |> ignore ArrayPool.Shared.Return(chars) stringBuilder.Clear() @@ -71,4 +71,4 @@ type HtmlUtf8Benchmark() = let main args = let asm = typeof.Assembly BenchmarkSwitcher.FromAssembly(asm).Run(args) |> ignore - 0 + 0 \ No newline at end of file diff --git a/tests/Giraffe.Tests/FormatExpressionTests.fs b/tests/Giraffe.Tests/FormatExpressionTests.fs index 0b71a38a..39242dc9 100644 --- a/tests/Giraffe.Tests/FormatExpressionTests.fs +++ b/tests/Giraffe.Tests/FormatExpressionTests.fs @@ -200,6 +200,20 @@ let ``Format string with single "%O" matches "FE9CFE1935D44EDC9A955D38C4D579BD"` | None -> assertFail "Format failed to match input." | Some g -> Assert.Equal(Guid("FE9CFE19-35D4-4EDC-9A95-5D38C4D579BD"), g) +[] +let ``Format string with single "%O" matches "Xy0MVKupFES9NpmZ9TiHcw"`` () = + tryMatchInput "%O" "Xy0MVKupFES9NpmZ9TiHcw" false + |> function + | None -> assertFail "Format failed to match input." + | Some g -> Assert.Equal(Guid("540c2d5f-a9ab-4414-bd36-9999f5388773"), g) + +[] +let ``Format string with single "%u" matches "FOwfPLe6waQ"`` () = + tryMatchInput "%u" "FOwfPLe6waQ" false + |> function + | None -> assertFail "Format failed to match input." + | Some id -> Assert.Equal(1507614320903242148UL, id) + [] let ``Format string with "%s" matches url encoded string`` () = tryMatchInput "/encode/%s" "/encode/a%2fb%2Bc.d%2Ce" false diff --git a/tests/Giraffe.Tests/Giraffe.Tests.fsproj b/tests/Giraffe.Tests/Giraffe.Tests.fsproj index 43c02c65..27e693ea 100644 --- a/tests/Giraffe.Tests/Giraffe.Tests.fsproj +++ b/tests/Giraffe.Tests/Giraffe.Tests.fsproj @@ -8,6 +8,7 @@ + diff --git a/tests/Giraffe.Tests/GuidAndIdTests.fs b/tests/Giraffe.Tests/GuidAndIdTests.fs new file mode 100644 index 00000000..ae5bad8d --- /dev/null +++ b/tests/Giraffe.Tests/GuidAndIdTests.fs @@ -0,0 +1,113 @@ +module Giraffe.Tests.ShortGuidTests + +open System +open Xunit +open Giraffe.Common + +// --------------------------------- +// Short Guid Tests +// --------------------------------- + +let rndInt64 (rand : Random) = + let buffer = Array.zeroCreate 8 + rand.NextBytes buffer + BitConverter.ToUInt64(buffer, 0) + +[] +let ``Short Guids translate to correct long Guids`` () = + let testCases = + [ + "FEx1sZbSD0ugmgMAF_RGHw", Guid "b1754c14-d296-4b0f-a09a-030017f4461f" + "Xy0MVKupFES9NpmZ9TiHcw", Guid "540c2d5f-a9ab-4414-bd36-9999f5388773" + ] + + testCases + |> List.iter (fun (shortGuid, expectedGuid) -> + let guid = ShortGuid.toGuid shortGuid + Assert.Equal(expectedGuid, guid) + |> ignore) + +[] +let ``Long Guids translate to correct short Guids`` () = + let testCases = + [ + "FEx1sZbSD0ugmgMAF_RGHw", Guid "b1754c14-d296-4b0f-a09a-030017f4461f" + "Xy0MVKupFES9NpmZ9TiHcw", Guid "540c2d5f-a9ab-4414-bd36-9999f5388773" + ] + + testCases + |> List.iter (fun (shortGuid, longGuid) -> + let guid = ShortGuid.fromGuid longGuid + Assert.Equal(shortGuid, guid) + |> ignore) + +[] +let ``Short Guids are always 22 characters long`` () = + let testCases = + [ 0..10 ] + |> List.map (fun _ -> Guid.NewGuid()) + + testCases + |> List.iter (fun guid -> + let shortGuid = ShortGuid.fromGuid guid + Assert.Equal(22, shortGuid.Length) + |> ignore) + +[] +let ``Short Ids are always 11 characters long`` () = + let rand = new Random() + let testCases = + [ 0..10 ] + |> List.map (fun _ -> rndInt64 rand) + + testCases + |> List.iter (fun id -> + let shortId = ShortId.fromUInt64 id + Assert.Equal(11, shortId.Length) + |> ignore) + +[] +let ``Short Ids translate correctly back and forth`` () = + let rand = new Random() + let testCases = + [ 0..10 ] + |> List.map (fun _ -> rndInt64 rand) + + testCases + |> List.iter (fun origId -> + let shortId = ShortId.fromUInt64 origId + let id = ShortId.toUInt64 shortId + Assert.Equal(origId, id) + |> ignore) + +[] +let ``Short Ids translate to correct uint64 values`` () = + let testCases = + [ + "r1iKapqh_s4", 12635000945053400782UL + "5aLu720NzTs", 16547050693006839099UL + "BdQ5vc0d8-I", 420024152605193186UL + "FOwfPLe6waQ", 1507614320903242148UL + ] + + testCases + |> List.iter (fun (shortId, id) -> + let result = ShortId.toUInt64 shortId + Assert.Equal(id, result) + |> ignore) + +[] +let ``UInt64 values translate to correct short IDs`` () = + let testCases = + [ + "r1iKapqh_s4", 12635000945053400782UL + "5aLu720NzTs", 16547050693006839099UL + "BdQ5vc0d8-I", 420024152605193186UL + "FOwfPLe6waQ", 1507614320903242148UL + ] + + testCases + |> List.iter (fun (shortId, id) -> + let result = ShortId.fromUInt64 id + Assert.Equal(shortId, result) + |> ignore) \ No newline at end of file diff --git a/tests/Giraffe.Tests/RoutingTests.fs b/tests/Giraffe.Tests/RoutingTests.fs index f7ea0a09..4aa8d2bc 100644 --- a/tests/Giraffe.Tests/RoutingTests.fs +++ b/tests/Giraffe.Tests/RoutingTests.fs @@ -363,7 +363,7 @@ let ``routef: GET "/foo/%O/bar/%O" returns "Guid1: ..., Guid2: ..."`` () = route "/foo" >=> text "bar" routef "/foo/%s/bar" text routef "/foo/%s/%i" (fun (name, age) -> text (sprintf "Name: %s, Age: %d" name age)) - routef "/foo/%O/bar/%O" (fun (guid1: Guid, guid2: Guid) -> text (sprintf "Guid1: %O, Guid2: %O" guid1 guid2)) + routef "/foo/%O/bar/%O" (fun (guid1 : Guid, guid2 : Guid) -> text (sprintf "Guid1: %O, Guid2: %O" guid1 guid2)) setStatusCode 404 >=> text "Not found" ] ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore @@ -379,6 +379,31 @@ let ``routef: GET "/foo/%O/bar/%O" returns "Guid1: ..., Guid2: ..."`` () = | Some ctx -> Assert.Equal(expected, getBody ctx) } +[] +let ``routef: GET "/foo/%u/bar/%u" returns "Id1: ..., Id2: ..."`` () = + let ctx = Substitute.For() + let app = + GET >=> choose [ + route "/" >=> text "Hello World" + route "/foo" >=> text "bar" + routef "/foo/%s/bar" text + routef "/foo/%s/%i" (fun (name, age) -> text (sprintf "Name: %s, Age: %d" name age)) + routef "/foo/%u/bar/%u" (fun (id1 : uint64, id2 : uint64) -> text (sprintf "Id1: %u, Id2: %u" id1 id2)) + setStatusCode 404 >=> text "Not found" ] + + ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore + ctx.Request.Path.ReturnsForAnyArgs (PathString("/foo/r1iKapqh_s4/bar/5aLu720NzTs")) |> ignore + ctx.Response.Body <- new MemoryStream() + let expected = "Id1: 12635000945053400782, Id2: 16547050693006839099" + + task { + let! result = app next ctx + + match result with + | None -> assertFailf "Result was expected to be %s" expected + | Some ctx -> Assert.Equal(expected, getBody ctx) + } + // --------------------------------- // routeCif Tests // --------------------------------- From f5506eb857ecb4ad27ef067bc54ae49024e29c90 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Sat, 15 Sep 2018 13:48:37 +0100 Subject: [PATCH 29/43] Updated RELEASE_NOTES Updated release notes for the next release. --- RELEASE_NOTES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index dbd1749f..a5fd9ef5 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -15,9 +15,10 @@ This change should not affect the majority of Giraffe users unless you were cons - Significant performance gains by changing the underlying way of how views are being composed by the `GriaffeViewEngine`. -#### New featurs +#### New features - Support for short GUIDs and short IDs (aka YouTube IDs) [in route arguments](https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#routef) and [query string parameters](https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#short-guids-and-short-ids). +- Enabled [SourceLink](https://github.com/dotnet/sourcelink/) support for Giraffe source code (thanks [Cameron Taggart](https://github.com/ctaggart))! For more information check out [Adding SourceLink to your .NET Core Library](https://carlos.mendible.com/2018/08/25/adding-sourcelink-to-your-net-core-library/). ## 2.0.1 From dd54cf1d283fea3c21a389eee9fe259ce9864b23 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Sat, 15 Sep 2018 19:35:52 +0100 Subject: [PATCH 30/43] Updated README with latest Benchmarks tests Fixes #277 --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ecb1bc9c..238d411e 100644 --- a/README.md +++ b/README.md @@ -152,9 +152,15 @@ More sample applications will be added in the future. ## Benchmarks -Currently Giraffe has only been tested against a simple plain text route and measured the total amount of handled requests per second. The latest result yielded an average of 79093 req/s over a period of 10 seconds, which was only closely after plain Kestrel which was capable of handling 79399 req/s on average. +Giraffe is part of the [TechEmpower Web Framework Benchmarks](https://www.techempower.com/benchmarks/#section=test&runid=a1843d12-6091-4780-92a6-a747fab77cb1&hw=ph&test=plaintext&l=hra0hp-1&p=zik0zj-zik0zj-zijocf-5m9r) and will be listed in the official results page in the upcoming Round 17 for the first time. -Please check out [Jimmy Byrd](https://github.com/TheAngryByrd)'s [dotnet-web-benchmarks](https://github.com/TheAngryByrd/dotnet-web-benchmarks) for more details. +Unofficial test results are currently available on the [TFB Status page](https://tfb-status.techempower.com/). + +As of today Giraffe competes in the Plaintext, JSON and Fortunes categories and has been doing pretty well so far, even outperforming ASP.NET Core MVC in Plaintext and JSON at the time of writing. + +The latest implementation which is being used for the benchmark tests can be seen inside the [TechEmpower repository](https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/frameworks/FSharp/giraffe). + +Giraffe is also featured in [Jimmy Byrd](https://github.com/TheAngryByrd)'s [dotnet-web-benchmarks](https://github.com/TheAngryByrd/dotnet-web-benchmarks) where we've run earlier performance tests. ## Building and developing From 0f19525287c275acc7db428bc86dc7225e75f869 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Sat, 15 Sep 2018 19:48:25 +0100 Subject: [PATCH 31/43] Updated manual install steps Updated installation steps based on feedback in #270 and #248. Fixes #270 Fixes #248 --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 238d411e..2b04c7ae 100644 --- a/README.md +++ b/README.md @@ -61,12 +61,18 @@ For more information about the Giraffe template please visit the official [giraf ### Doing it manually -Install the [Giraffe](https://www.nuget.org/packages/Giraffe) NuGet package: +Install the [Giraffe](https://www.nuget.org/packages/Giraffe) NuGet package*: ``` PM> Install-Package Giraffe ``` +*) If you haven't installed the ASP.NET Core NuGet package yet then you'll also need to add a package reference to `Microsoft.AspNetCore.App`: + +``` +PM> Install-Package Microsoft.AspNetCore.App +``` + Create a web application and plug it into the ASP.NET Core middleware: ```fsharp From a64ade694a01f32fcabf85f14761a720f719c318 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Sat, 15 Sep 2018 19:48:44 +0100 Subject: [PATCH 32/43] Fixes #248 README updates --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b04c7ae..39bb9f1e 100644 --- a/README.md +++ b/README.md @@ -73,9 +73,20 @@ PM> Install-Package Giraffe PM> Install-Package Microsoft.AspNetCore.App ``` -Create a web application and plug it into the ASP.NET Core middleware: +Alternatively you can also use the .NET CLI to add the packages: + +``` +dotnet add [PROJECT] package Microsoft.AspNetCore.App --package-directory [PACKAGE_CIRECTORY] +dotnet add [PROJECT] package Giraffe --package-directory [PACKAGE_CIRECTORY] +``` + +Next create a web application and plug it into the ASP.NET Core middleware: ```fsharp +open System +open Microsoft.AspNetCore.Builder +open Microsoft.AspNetCore.Hosting +open Microsoft.Extensions.DependencyInjection open Giraffe let webApp = @@ -107,6 +118,10 @@ let main _ = Instead of creating a `Startup` class you can also add Giraffe in a more functional way: ```fsharp +open System +open Microsoft.AspNetCore.Builder +open Microsoft.AspNetCore.Hosting +open Microsoft.Extensions.DependencyInjection open Giraffe let webApp = From d3c381ab1de739e68c4fb07b0858694bc1a949d3 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Sun, 16 Sep 2018 07:47:20 +0100 Subject: [PATCH 33/43] Moved SourceLink settings to Giraffe.fsproj Removed Directory.Build.props --- src/Directory.Build.props | 13 ------------- src/Giraffe/Giraffe.fsproj | 6 ++++++ 2 files changed, 6 insertions(+), 13 deletions(-) delete mode 100644 src/Directory.Build.props diff --git a/src/Directory.Build.props b/src/Directory.Build.props deleted file mode 100644 index b5fa4167..00000000 --- a/src/Directory.Build.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - true - true - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - - FS2003 - - - - - \ No newline at end of file diff --git a/src/Giraffe/Giraffe.fsproj b/src/Giraffe/Giraffe.fsproj index 567de758..e2dc9357 100644 --- a/src/Giraffe/Giraffe.fsproj +++ b/src/Giraffe/Giraffe.fsproj @@ -28,9 +28,15 @@ true git https://github.com/dustinmoris/giraffe + + + true + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + From c2ca73c7674c09955ad8773d6754f809785f57c8 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Sun, 16 Sep 2018 09:13:21 +0100 Subject: [PATCH 34/43] Added Utf8JsonSerializer Playing with the idea of swapping a faster, better JSON serializer for Newtonsoft.Json --- src/Giraffe/Giraffe.fsproj | 1 + src/Giraffe/Middleware.fs | 2 +- src/Giraffe/ResponseWriters.fs | 2 +- src/Giraffe/Serialization.fs | 65 +++++++++++++++++++++++----------- 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/src/Giraffe/Giraffe.fsproj b/src/Giraffe/Giraffe.fsproj index 567de758..5b21b3ab 100644 --- a/src/Giraffe/Giraffe.fsproj +++ b/src/Giraffe/Giraffe.fsproj @@ -38,6 +38,7 @@ + diff --git a/src/Giraffe/Middleware.fs b/src/Giraffe/Middleware.fs index 56237b31..34cd0fa1 100644 --- a/src/Giraffe/Middleware.fs +++ b/src/Giraffe/Middleware.fs @@ -122,7 +122,7 @@ type IServiceCollection with /// Returns an `IServiceCollection` builder object. /// member this.AddGiraffe() = - this.TryAddSingleton(NewtonsoftJsonSerializer(NewtonsoftJsonSerializer.DefaultSettings)) + this.TryAddSingleton() this.TryAddSingleton(DefaultXmlSerializer(DefaultXmlSerializer.DefaultSettings)) this.TryAddSingleton() this \ No newline at end of file diff --git a/src/Giraffe/ResponseWriters.fs b/src/Giraffe/ResponseWriters.fs index cd1cf12e..c0ad1f01 100644 --- a/src/Giraffe/ResponseWriters.fs +++ b/src/Giraffe/ResponseWriters.fs @@ -105,7 +105,7 @@ type HttpContext with member this.WriteJsonChunkedAsync<'T> (dataObj : 'T) = task { this.SetContentType "application/json" let serializer = this.GetJsonSerializer() - do! serializer.SerializeAsync (dataObj, this.Response.Body) + do! serializer.SerializeToStreamAsync dataObj this.Response.Body return Some this } diff --git a/src/Giraffe/Serialization.fs b/src/Giraffe/Serialization.fs index f8fac9c8..6c9b8549 100644 --- a/src/Giraffe/Serialization.fs +++ b/src/Giraffe/Serialization.fs @@ -6,11 +6,13 @@ namespace Giraffe.Serialization [] module Json = + open System open System.IO + open System.Text open System.Threading.Tasks open Newtonsoft.Json open Newtonsoft.Json.Serialization - open System.Text + open Utf8Json /// **Description** /// @@ -18,12 +20,34 @@ module Json = /// [] type IJsonSerializer = - abstract member Serialize<'T> : 'T -> string - abstract member SerializeToBytes<'T> : 'T -> byte[] - abstract member SerializeAsync<'T> : 'T * Stream -> Task - abstract member Deserialize<'T> : string -> 'T - abstract member Deserialize<'T> : Stream -> 'T - abstract member DeserializeAsync<'T> : Stream -> Task<'T> + abstract member SerializeToString<'T> : 'T -> string + abstract member SerializeToBytes<'T> : 'T -> byte array + abstract member SerializeToStreamAsync<'T> : 'T -> Stream -> Task + + abstract member Deserialize<'T> : string -> 'T + abstract member Deserialize<'T> : byte[] -> 'T + abstract member DeserializeAsync<'T> : Stream -> Task<'T> + + type Utf8JsonSerializer () = + interface IJsonSerializer with + member __.SerializeToString (x : 'T) = + JsonSerializer.ToJsonString x + + member __.SerializeToBytes (x : 'T) = + JsonSerializer.Serialize x + + member __.SerializeToStreamAsync (x : 'T) (stream : Stream) = + JsonSerializer.SerializeAsync(stream, x) + + member __.Deserialize<'T> (json : string) : 'T = + Encoding.UTF8.GetBytes json + |> JsonSerializer.Deserialize + + member __.Deserialize<'T> (bytes : byte array) : 'T = + JsonSerializer.Deserialize bytes + + member __.DeserializeAsync<'T> (stream : Stream) : Task<'T> = + JsonSerializer.DeserializeAsync stream /// **Description** /// @@ -32,7 +56,7 @@ module Json = /// Serializes objects to camel cased JSON code. /// type NewtonsoftJsonSerializer (settings : JsonSerializerSettings) = - + let Utf8EncodingWithoutBom = new UTF8Encoding(false) let DefaultBufferSize = 1024 @@ -41,33 +65,32 @@ module Json = ContractResolver = CamelCasePropertyNamesContractResolver()) interface IJsonSerializer with - member __.Serialize (o : 'T) = JsonConvert.SerializeObject(o, settings) + member __.SerializeToString (x : 'T) = + JsonConvert.SerializeObject(x, settings) - member __.SerializeToBytes (o: 'T) = - let json = JsonConvert.SerializeObject(o, settings) - Encoding.UTF8.GetBytes(json) + member __.SerializeToBytes (x : 'T) = + JsonConvert.SerializeObject(x, settings) + |> Encoding.UTF8.GetBytes - member __.SerializeAsync (o: 'T, stream: Stream) = + member __.SerializeToStreamAsync (x : 'T) (stream : Stream) = use sw = new StreamWriter(stream, Utf8EncodingWithoutBom, DefaultBufferSize, true) use jw = new JsonTextWriter(sw) let sr = JsonSerializer.Create settings - sr.Serialize(jw, o) + sr.Serialize(jw, x) Task.CompletedTask - member __.Deserialize<'T> (json : string) = + member __.Deserialize<'T> (json : string) = JsonConvert.DeserializeObject<'T>(json, settings) - member __.Deserialize<'T> (stream : Stream) = - use sr = new StreamReader(stream, true) - use jr = new JsonTextReader(sr) - let sr = JsonSerializer.Create settings - sr.Deserialize<'T> jr + member __.Deserialize<'T> (bytes : byte array) = + let json = Encoding.UTF8.GetString bytes + JsonConvert.DeserializeObject<'T>(json, settings) member __.DeserializeAsync<'T> (stream : Stream) = use sr = new StreamReader(stream, true) use jr = new JsonTextReader(sr) let sr = JsonSerializer.Create settings - Task.FromResult( sr.Deserialize<'T> jr ) + Task.FromResult(sr.Deserialize<'T> jr) // --------------------------- // XML From 70e3e246e581372733fcd042327772b0900cf4fc Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Sun, 16 Sep 2018 09:48:22 +0100 Subject: [PATCH 35/43] Added resolver as constructor argument to Utf8JsonSerializer Added the resolver like shown in the examples. --- src/Giraffe/Middleware.fs | 2 +- src/Giraffe/Serialization.fs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Giraffe/Middleware.fs b/src/Giraffe/Middleware.fs index 34cd0fa1..4a1b39f6 100644 --- a/src/Giraffe/Middleware.fs +++ b/src/Giraffe/Middleware.fs @@ -122,7 +122,7 @@ type IServiceCollection with /// Returns an `IServiceCollection` builder object. /// member this.AddGiraffe() = - this.TryAddSingleton() + this.TryAddSingleton(Utf8JsonSerializer(Utf8Json.JsonSerializer.DefaultResolver)) this.TryAddSingleton(DefaultXmlSerializer(DefaultXmlSerializer.DefaultSettings)) this.TryAddSingleton() this \ No newline at end of file diff --git a/src/Giraffe/Serialization.fs b/src/Giraffe/Serialization.fs index 6c9b8549..f3612771 100644 --- a/src/Giraffe/Serialization.fs +++ b/src/Giraffe/Serialization.fs @@ -28,26 +28,26 @@ module Json = abstract member Deserialize<'T> : byte[] -> 'T abstract member DeserializeAsync<'T> : Stream -> Task<'T> - type Utf8JsonSerializer () = + type Utf8JsonSerializer (resolver : IJsonFormatterResolver) = interface IJsonSerializer with member __.SerializeToString (x : 'T) = - JsonSerializer.ToJsonString x + JsonSerializer.ToJsonString (x, resolver) member __.SerializeToBytes (x : 'T) = - JsonSerializer.Serialize x + JsonSerializer.Serialize (x, resolver) member __.SerializeToStreamAsync (x : 'T) (stream : Stream) = - JsonSerializer.SerializeAsync(stream, x) + JsonSerializer.SerializeAsync(stream, x, resolver) member __.Deserialize<'T> (json : string) : 'T = - Encoding.UTF8.GetBytes json - |> JsonSerializer.Deserialize + let bytes = Encoding.UTF8.GetBytes json + JsonSerializer.Deserialize(bytes, resolver) member __.Deserialize<'T> (bytes : byte array) : 'T = - JsonSerializer.Deserialize bytes + JsonSerializer.Deserialize(bytes, resolver) member __.DeserializeAsync<'T> (stream : Stream) : Task<'T> = - JsonSerializer.DeserializeAsync stream + JsonSerializer.DeserializeAsync(stream, resolver) /// **Description** /// From 217aea029cb5a24cbfd7347a419454d620ab2479 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Sun, 16 Sep 2018 09:59:58 +0100 Subject: [PATCH 36/43] Changed XML comments Also added the Transfer-Encoding HTTP header for chucked JSON transfer --- src/Giraffe/ResponseWriters.fs | 5 +++-- src/Giraffe/Serialization.fs | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Giraffe/ResponseWriters.fs b/src/Giraffe/ResponseWriters.fs index c0ad1f01..b6d26ad5 100644 --- a/src/Giraffe/ResponseWriters.fs +++ b/src/Giraffe/ResponseWriters.fs @@ -90,7 +90,7 @@ type HttpContext with /// /// Serializes an object to JSON and writes the output to the body of the HTTP response using chunked transfer encoding. /// - /// It also sets the HTTP `Content-Type` header to `application/json` and sets the `Content-Length` header accordingly. + /// It also sets the HTTP `Content-Type` header to `application/json` and sets the `Transfer-Encoding` header to `chunked`. /// /// The JSON serializer can be configured in the ASP.NET Core startup code by registering a custom class of type `IJsonSerializer`. /// @@ -104,6 +104,7 @@ type HttpContext with /// member this.WriteJsonChunkedAsync<'T> (dataObj : 'T) = task { this.SetContentType "application/json" + this.SetHttpHeader "Transfer-Encoding" "chunked" let serializer = this.GetJsonSerializer() do! serializer.SerializeToStreamAsync dataObj this.Response.Body return Some this @@ -277,7 +278,7 @@ let json<'T> (dataObj : 'T) : HttpHandler = /// /// Serializes an object to JSON and writes the output to the body of the HTTP response using chunked transfer encoding. /// -/// It also sets the HTTP `Content-Type` header to `application/json` and sets the `Content-Length` header accordingly. +/// It also sets the HTTP `Content-Type` header to `application/json` and sets the `Transfer-Encoding` header to `chunked`. /// /// The JSON serializer can be configured in the ASP.NET Core startup code by registering a custom class of type `IJsonSerializer`. /// diff --git a/src/Giraffe/Serialization.fs b/src/Giraffe/Serialization.fs index f3612771..7eea183f 100644 --- a/src/Giraffe/Serialization.fs +++ b/src/Giraffe/Serialization.fs @@ -28,6 +28,14 @@ module Json = abstract member Deserialize<'T> : byte[] -> 'T abstract member DeserializeAsync<'T> : Stream -> Task<'T> + /// **Description** + /// + /// The `Utf8JsonSerializer` is the default `IJsonSerializer` in Giraffe. + /// + /// It uses `Utf8Json` as the underlying JSON serializer to (de-)serialize + /// JSON content. [Utf8Json](https://github.com/neuecc/Utf8Json) is currently + /// the fastest JSON serializer for .NET. + /// type Utf8JsonSerializer (resolver : IJsonFormatterResolver) = interface IJsonSerializer with member __.SerializeToString (x : 'T) = @@ -51,7 +59,15 @@ module Json = /// **Description** /// - /// Default JSON serializer in Giraffe. + /// The previous default JSON serializer in Giraffe. + /// + /// The `NewtonsoftJsonSerializer` has been replaced by `Utf8JsonSerializer` as + /// the default `IJsonSerializer` which has much better performance and supports + /// true chunked transfer encoding. + /// + /// The `NewtonsoftJsonSerializer` remains available as an alternative JSON + /// serializer which can be used to override the `Utf8JsonSerializer` for + /// backwards compatibility. /// /// Serializes objects to camel cased JSON code. /// From 6bdb5d1621785a4f4800df0558bb96f0694d8d1b Mon Sep 17 00:00:00 2001 From: Dmytro Kushnir Date: Sun, 16 Sep 2018 15:13:51 +0300 Subject: [PATCH 37/43] added tests for utf8 resolver --- src/Giraffe/Serialization.fs | 3 + tests/Giraffe.Tests/Helpers.fs | 67 ++++++++++++++++++--- tests/Giraffe.Tests/HttpHandlerTests.fs | 75 ++++++++++++------------ tests/Giraffe.Tests/ModelBindingTests.fs | 28 +++++---- tests/Giraffe.Tests/RoutingTests.fs | 8 ++- 5 files changed, 119 insertions(+), 62 deletions(-) diff --git a/src/Giraffe/Serialization.fs b/src/Giraffe/Serialization.fs index 7eea183f..8a340905 100644 --- a/src/Giraffe/Serialization.fs +++ b/src/Giraffe/Serialization.fs @@ -37,6 +37,9 @@ module Json = /// the fastest JSON serializer for .NET. /// type Utf8JsonSerializer (resolver : IJsonFormatterResolver) = + + static member DefaultResolver = Utf8Json.Resolvers.StandardResolver.CamelCase + interface IJsonSerializer with member __.SerializeToString (x : 'T) = JsonSerializer.ToJsonString (x, resolver) diff --git a/tests/Giraffe.Tests/Helpers.fs b/tests/Giraffe.Tests/Helpers.fs index 4ede18a4..39bd5450 100644 --- a/tests/Giraffe.Tests/Helpers.fs +++ b/tests/Giraffe.Tests/Helpers.fs @@ -19,11 +19,24 @@ open NSubstitute open Newtonsoft.Json open Giraffe open Giraffe.Serialization +open Utf8Json // --------------------------------- // Common functions // --------------------------------- +let toTheoryData xs = + let data = new TheoryData<_>() + for x in xs do + data.Add x + data + +let toTheoryData2 xs = + let data = new TheoryData<_,_>() + for (a,b) in xs do + data.Add(a,b) + data + let waitForDebuggerToAttach() = printfn "Waiting for debugger to attach." printfn "Press enter when debugger is attached in order to continue test execution..." @@ -97,14 +110,50 @@ let createHost (configureApp : 'Tuple -> IApplicationBuilder -> unit) .Configure(Action (configureApp args)) .ConfigureServices(Action configureServices) -let mockJson (ctx : HttpContext) (settings : JsonSerializerSettings option) = - let jsonSettings = - defaultArg settings NewtonsoftJsonSerializer.DefaultSettings - ctx.RequestServices - .GetService(typeof) - .Returns(NewtonsoftJsonSerializer(jsonSettings)) - |> ignore +type MockJsonSettings = + | Newtonsoft of JsonSerializerSettings option + | Utf8 of IJsonFormatterResolver option + +let mockJson (ctx : HttpContext) (settings : MockJsonSettings) = + + match settings with + | Newtonsoft settings -> + let jsonSettings = + defaultArg settings NewtonsoftJsonSerializer.DefaultSettings + + ctx.RequestServices + .GetService(typeof) + .Returns(new NewtonsoftJsonSerializer(jsonSettings)) + |> ignore + + | Utf8 settings -> + + let resolver = + defaultArg settings Utf8JsonSerializer.DefaultResolver + + ctx.RequestServices + .GetService(typeof) + .Returns(new Utf8JsonSerializer(resolver)) + |> ignore + +type JsonSerializersData = + + static member DefaultSettings = [ + Utf8 None; + Newtonsoft None + ] + + static member DefaultData = JsonSerializersData.DefaultSettings |> toTheoryData + + static member PreserveCaseSettings = + [ + Utf8 (Some Utf8Json.Resolvers.StandardResolver.Default) + Newtonsoft (Some <| new JsonSerializerSettings()) + ] + + static member PreserveCaseData = JsonSerializersData.PreserveCaseSettings |> toTheoryData + let mockXml (ctx : HttpContext) = ctx.RequestServices .GetService(typeof) @@ -208,4 +257,6 @@ let shouldBeEmpty (bytes : byte[]) = Assert.True(bytes.Length.Equals 0) let shouldEqual expected actual = - Assert.Equal(expected, actual) \ No newline at end of file + Assert.Equal(expected, actual) + + diff --git a/tests/Giraffe.Tests/HttpHandlerTests.fs b/tests/Giraffe.Tests/HttpHandlerTests.fs index 9b24e519..cb3c4f9d 100644 --- a/tests/Giraffe.Tests/HttpHandlerTests.fs +++ b/tests/Giraffe.Tests/HttpHandlerTests.fs @@ -44,10 +44,11 @@ type Person = // Tests // --------------------------------- -[] -let ``GET "/json" returns json object`` () = +[] +[)>] +let ``GET "/json" returns json object`` (settings) = let ctx = Substitute.For() - mockJson ctx None + mockJson ctx settings let app = GET >=> choose [ route "/" >=> text "Hello World" @@ -68,11 +69,12 @@ let ``GET "/json" returns json object`` () = | Some ctx -> Assert.Equal(expected, getBody ctx) } -[] -let ``GET "/json" with custom json settings returns json object`` () = - let settings = Newtonsoft.Json.JsonSerializerSettings() +[] +[)>] +let ``GET "/json" with custom json settings returns json object`` (settings) = + let ctx = Substitute.For() - mockJson ctx (Some settings) + mockJson ctx settings let app = GET >=> choose [ route "/" >=> text "Hello World" @@ -93,21 +95,19 @@ let ``GET "/json" with custom json settings returns json object`` () = | Some ctx -> Assert.Equal(expected, getBody ctx) } +let DefaultMocksWithSize = + [ + let ``powers of two`` = [1..10] |> List.map (pown 2) + for size in ``powers of two`` do + for setting in JsonSerializersData.DefaultSettings do + yield size ,setting + ] |> toTheoryData2 + [] -[] -[] -[] -[] -[] -[] -[] -[] -[] -[] -[] -let ``GET "/jsonChunked" returns json object`` (size: int) = +[] +let ``GET "/jsonChunked" returns json object`` (size: int, settings) = let ctx = Substitute.For() - mockJson ctx None + mockJson ctx settings let app = GET >=> choose [ route "/" >=> text "Hello World" @@ -132,22 +132,19 @@ let ``GET "/jsonChunked" returns json object`` (size: int) = | Some ctx -> Assert.Equal(expected, getBody ctx) } +let CamelCasedMocksWithSize = + [ + let ``powers of two`` = [1..10] |> List.map (pown 2) + for size in ``powers of two`` do + for setting in JsonSerializersData.PreserveCaseSettings do + yield size ,setting + ] |> toTheoryData2 + [] -[] -[] -[] -[] -[] -[] -[] -[] -[] -[] -[] -let ``GET "/jsonChunked" with custom json settings returns json object`` (size: int) = - let settings = Newtonsoft.Json.JsonSerializerSettings() +[] +let ``GET "/jsonChunked" with custom json settings returns json object`` (size: int, settings) = let ctx = Substitute.For() - mockJson ctx (Some settings) + mockJson ctx settings let app = GET >=> choose [ route "/" >=> text "Hello World" @@ -289,7 +286,7 @@ let ``POST "/text" with supported Accept header returns "text"`` () = [] let ``POST "/json" with supported Accept header returns "json"`` () = let ctx = Substitute.For() - mockJson ctx None + mockJson ctx ( Newtonsoft None ) let app = choose [ GET >=> choose [ @@ -439,7 +436,7 @@ let ``Get "/auto" with Accept header of "application/json" returns JSON object`` } let ctx = Substitute.For() - mockJson ctx None + mockJson ctx (Newtonsoft None) mockNegotiation ctx let app = GET >=> choose [ @@ -481,7 +478,7 @@ let ``Get "/auto" with Accept header of "application/xml; q=0.9, application/jso } let ctx = Substitute.For() - mockJson ctx None + mockJson ctx (Newtonsoft None) mockNegotiation ctx let app = GET >=> choose [ @@ -627,7 +624,7 @@ let ``Get "/auto" with Accept header of "application/json, application/xml" retu } let ctx = Substitute.For() - mockJson ctx None + mockJson ctx (Newtonsoft None) mockNegotiation ctx let app = GET >=> choose [ @@ -860,7 +857,7 @@ let ``Get "/auto" without an Accept header returns a JSON object`` () = } let ctx = Substitute.For() - mockJson ctx None + mockJson ctx (Newtonsoft None) mockNegotiation ctx let app = GET >=> choose [ diff --git a/tests/Giraffe.Tests/ModelBindingTests.fs b/tests/Giraffe.Tests/ModelBindingTests.fs index c57211e2..9af52bc8 100644 --- a/tests/Giraffe.Tests/ModelBindingTests.fs +++ b/tests/Giraffe.Tests/ModelBindingTests.fs @@ -547,10 +547,11 @@ let ``tryBindQuery with complete data but baldy formated list items`` () = // HttpContext Model Binding Tests // --------------------------------- -[] -let ``BindJsonAsync test`` () = +[] +[)>] +let ``BindJsonAsync test`` (settings) = let ctx = Substitute.For() - mockJson ctx None + mockJson ctx settings let outputCustomer (c : Customer) = text (c.ToString()) let app = @@ -769,10 +770,11 @@ let ``BindQueryString with nullable property test`` () = return! testRoute "?NullableInt=&NullableDateTime=" { NullableInt = Nullable<_>(); NullableDateTime = Nullable<_>() } } -[] -let ``BindModelAsync with JSON content returns correct result`` () = +[] +[)>] +let ``BindModelAsync with JSON content returns correct result`` (settings) = let ctx = Substitute.For() - mockJson ctx None + mockJson ctx settings let outputCustomer (c : Customer) = text (c.ToString()) let app = @@ -807,10 +809,11 @@ let ``BindModelAsync with JSON content returns correct result`` () = | Some ctx -> Assert.Equal(expected, getBody ctx) } -[] -let ``BindModelAsync with JSON content that uses custom serialization settings returns correct result`` () = +[] +[)>] +let ``BindModelAsync with JSON content that uses custom serialization settings returns correct result`` (settings) = let ctx = Substitute.For() - mockJson ctx (Some (JsonSerializerSettings())) + mockJson ctx settings let outputCustomer (c : Customer) = text (c.ToString()) let app = @@ -962,10 +965,11 @@ let ``BindModelAsync with culture aware form content returns correct result`` () | Some ctx -> Assert.Equal(expected, getBody ctx) } -[] -let ``BindModelAsync with JSON content and a specific charset returns correct result`` () = +[] +[)>] +let ``BindModelAsync with JSON content and a specific charset returns correct result`` (settings) = let ctx = Substitute.For() - mockJson ctx None + mockJson ctx settings let outputCustomer (c : Customer) = text (c.ToString()) let app = diff --git a/tests/Giraffe.Tests/RoutingTests.fs b/tests/Giraffe.Tests/RoutingTests.fs index 4aa8d2bc..fbd32e66 100644 --- a/tests/Giraffe.Tests/RoutingTests.fs +++ b/tests/Giraffe.Tests/RoutingTests.fs @@ -86,10 +86,12 @@ let ``route: GET "/FOO" returns 404 "Not found"`` () = // routeCi Tests // --------------------------------- + + [] let ``GET "/JSON" returns "BaR"`` () = let ctx = Substitute.For() - mockJson ctx None + mockJson ctx (Newtonsoft None) let app = GET >=> choose [ route "/" >=> text "Hello World" @@ -411,7 +413,7 @@ let ``routef: GET "/foo/%u/bar/%u" returns "Id1: ..., Id2: ..."`` () = [] let ``POST "/POsT/1" returns "1"`` () = let ctx = Substitute.For() - mockJson ctx None + mockJson ctx (Newtonsoft None) let app = choose [ GET >=> choose [ @@ -437,7 +439,7 @@ let ``POST "/POsT/1" returns "1"`` () = [] let ``POST "/POsT/523" returns "523"`` () = let ctx = Substitute.For() - mockJson ctx None + mockJson ctx (Newtonsoft None) let app = choose [ GET >=> choose [ From 4f08e84400bd15a42b255fa8af1a2067b90ecca9 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Sun, 16 Sep 2018 13:15:05 +0100 Subject: [PATCH 38/43] Added missing nowarn Fixing build --- src/Giraffe/Giraffe.fsproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Giraffe/Giraffe.fsproj b/src/Giraffe/Giraffe.fsproj index e2dc9357..ff4cb835 100644 --- a/src/Giraffe/Giraffe.fsproj +++ b/src/Giraffe/Giraffe.fsproj @@ -33,6 +33,7 @@ true true $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + FS2003 From 3fedce32ddd64f401d64ddb20f5b469c1176a715 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Mon, 17 Sep 2018 09:00:02 +0100 Subject: [PATCH 39/43] Added first class support for response caching Added new http handlers to natively work with ASP.NET Core's response caching middleware, including the VaryByQueryKeys feature. Fixes #235 --- samples/SampleApp/SampleApp/Program.fs | 4 ++ src/Giraffe/Caching.fs | 54 ++++++++++++++++++++++++++ src/Giraffe/Giraffe.fsproj | 2 + 3 files changed, 60 insertions(+) create mode 100644 src/Giraffe/Caching.fs diff --git a/samples/SampleApp/SampleApp/Program.fs b/samples/SampleApp/SampleApp/Program.fs index 813de2ea..81b2e5d1 100644 --- a/samples/SampleApp/SampleApp/Program.fs +++ b/samples/SampleApp/SampleApp/Program.fs @@ -133,6 +133,8 @@ let webApp = route "/configured" >=> configuredHandler route "/upload" >=> fileUploadHandler route "/upload2" >=> fileUploadHandler2 + route "/cache/1" >=> publicResponseCaching 30 >=> warbler (fun _ -> text (Guid.NewGuid().ToString())) + route "/cache/2" >=> noResponseCaching >=> warbler (fun _ -> text (Guid.NewGuid().ToString())) ] route "/car" >=> bindModel None json route "/car2" >=> tryBindQuery parsingErrorHandler None (validateModel xml) @@ -151,12 +153,14 @@ let cookieAuth (o : CookieAuthenticationOptions) = let configureApp (app : IApplicationBuilder) = app.UseGiraffeErrorHandler(errorHandler) + .UseResponseCaching() .UseStaticFiles() .UseAuthentication() .UseGiraffe webApp let configureServices (services : IServiceCollection) = services + .AddResponseCaching() .AddGiraffe() .AddAuthentication(authScheme) .AddCookie(cookieAuth) |> ignore diff --git a/src/Giraffe/Caching.fs b/src/Giraffe/Caching.fs new file mode 100644 index 00000000..3ca97fd8 --- /dev/null +++ b/src/Giraffe/Caching.fs @@ -0,0 +1,54 @@ +[] +module Giraffe.Caching + +open System +open Microsoft.Extensions.Primitives +open Microsoft.Net.Http.Headers +open Microsoft.AspNetCore.Http +open Microsoft.AspNetCore.ResponseCaching + +type CacheDirective = + | NoCache + | Public of TimeSpan + | Private of TimeSpan + +let private noCacheHeader = CacheControlHeaderValue(NoCache = true, NoStore = true) + +let inline private cacheHeader isPublic duration = + CacheControlHeaderValue( + Public = isPublic, + MaxAge = Nullable duration) + + +let responseCaching (directive : CacheDirective) + (vary : string option) + (varyByQueryKeys : string array option) : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + + let tHeaders = ctx.Response.GetTypedHeaders() + let headers = ctx.Response.Headers + + match directive with + | NoCache -> + tHeaders.CacheControl <- noCacheHeader + headers.[ HeaderNames.Pragma ] <- StringValues [| "no-cache" |] + headers.[ HeaderNames.Expires ] <- StringValues [| "-1" |] + | Public duration -> tHeaders.CacheControl <- cacheHeader true duration + | Private duration -> tHeaders.CacheControl <- cacheHeader false duration + + if vary.IsSome then headers.[HeaderNames.Vary] <- StringValues [| vary.Value |] + + if varyByQueryKeys.IsSome then + let responseCachingFeature = ctx.Features.Get() + if isNotNull responseCachingFeature then + responseCachingFeature.VaryByQueryKeys <- varyByQueryKeys.Value + + next ctx + +let noResponseCaching : HttpHandler = responseCaching NoCache None None + +let privateResponseCaching (seconds : int) (vary : string option) : HttpHandler = + responseCaching (Private (TimeSpan.FromSeconds(float seconds))) vary None + +let publicResponseCaching (seconds : int) (vary : string option) : HttpHandler = + responseCaching (Public (TimeSpan.FromSeconds(float seconds))) vary None \ No newline at end of file diff --git a/src/Giraffe/Giraffe.fsproj b/src/Giraffe/Giraffe.fsproj index ff4cb835..cc82139d 100644 --- a/src/Giraffe/Giraffe.fsproj +++ b/src/Giraffe/Giraffe.fsproj @@ -44,6 +44,7 @@ + @@ -54,6 +55,7 @@ + From 808403b1b0c4b950efdb072008421311dd0fbabc Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Mon, 17 Sep 2018 14:34:39 +0100 Subject: [PATCH 40/43] Fixed broken build Fixed broken build and added XML comments to the new Response Caching http handlers. --- samples/SampleApp/SampleApp/Program.fs | 2 +- src/Giraffe/Caching.fs | 54 ------------ src/Giraffe/Common.fs | 3 +- src/Giraffe/Giraffe.fsproj | 2 +- src/Giraffe/ResponseCaching.fs | 117 +++++++++++++++++++++++++ 5 files changed, 120 insertions(+), 58 deletions(-) delete mode 100644 src/Giraffe/Caching.fs create mode 100644 src/Giraffe/ResponseCaching.fs diff --git a/samples/SampleApp/SampleApp/Program.fs b/samples/SampleApp/SampleApp/Program.fs index 81b2e5d1..36fb7321 100644 --- a/samples/SampleApp/SampleApp/Program.fs +++ b/samples/SampleApp/SampleApp/Program.fs @@ -133,7 +133,7 @@ let webApp = route "/configured" >=> configuredHandler route "/upload" >=> fileUploadHandler route "/upload2" >=> fileUploadHandler2 - route "/cache/1" >=> publicResponseCaching 30 >=> warbler (fun _ -> text (Guid.NewGuid().ToString())) + route "/cache/1" >=> publicResponseCaching 30 None >=> warbler (fun _ -> text (Guid.NewGuid().ToString())) route "/cache/2" >=> noResponseCaching >=> warbler (fun _ -> text (Guid.NewGuid().ToString())) ] route "/car" >=> bindModel None json diff --git a/src/Giraffe/Caching.fs b/src/Giraffe/Caching.fs deleted file mode 100644 index 3ca97fd8..00000000 --- a/src/Giraffe/Caching.fs +++ /dev/null @@ -1,54 +0,0 @@ -[] -module Giraffe.Caching - -open System -open Microsoft.Extensions.Primitives -open Microsoft.Net.Http.Headers -open Microsoft.AspNetCore.Http -open Microsoft.AspNetCore.ResponseCaching - -type CacheDirective = - | NoCache - | Public of TimeSpan - | Private of TimeSpan - -let private noCacheHeader = CacheControlHeaderValue(NoCache = true, NoStore = true) - -let inline private cacheHeader isPublic duration = - CacheControlHeaderValue( - Public = isPublic, - MaxAge = Nullable duration) - - -let responseCaching (directive : CacheDirective) - (vary : string option) - (varyByQueryKeys : string array option) : HttpHandler = - fun (next : HttpFunc) (ctx : HttpContext) -> - - let tHeaders = ctx.Response.GetTypedHeaders() - let headers = ctx.Response.Headers - - match directive with - | NoCache -> - tHeaders.CacheControl <- noCacheHeader - headers.[ HeaderNames.Pragma ] <- StringValues [| "no-cache" |] - headers.[ HeaderNames.Expires ] <- StringValues [| "-1" |] - | Public duration -> tHeaders.CacheControl <- cacheHeader true duration - | Private duration -> tHeaders.CacheControl <- cacheHeader false duration - - if vary.IsSome then headers.[HeaderNames.Vary] <- StringValues [| vary.Value |] - - if varyByQueryKeys.IsSome then - let responseCachingFeature = ctx.Features.Get() - if isNotNull responseCachingFeature then - responseCachingFeature.VaryByQueryKeys <- varyByQueryKeys.Value - - next ctx - -let noResponseCaching : HttpHandler = responseCaching NoCache None None - -let privateResponseCaching (seconds : int) (vary : string option) : HttpHandler = - responseCaching (Private (TimeSpan.FromSeconds(float seconds))) vary None - -let publicResponseCaching (seconds : int) (vary : string option) : HttpHandler = - responseCaching (Public (TimeSpan.FromSeconds(float seconds))) vary None \ No newline at end of file diff --git a/src/Giraffe/Common.fs b/src/Giraffe/Common.fs index 801edc09..146e6584 100644 --- a/src/Giraffe/Common.fs +++ b/src/Giraffe/Common.fs @@ -227,5 +227,4 @@ module ShortId = match BitConverter.IsLittleEndian with | true -> Array.Reverse arr; arr | false -> arr) - BitConverter.ToUInt64 (bytes, 0) - + BitConverter.ToUInt64 (bytes, 0) \ No newline at end of file diff --git a/src/Giraffe/Giraffe.fsproj b/src/Giraffe/Giraffe.fsproj index cc82139d..298e0879 100644 --- a/src/Giraffe/Giraffe.fsproj +++ b/src/Giraffe/Giraffe.fsproj @@ -55,7 +55,7 @@ - + diff --git a/src/Giraffe/ResponseCaching.fs b/src/Giraffe/ResponseCaching.fs new file mode 100644 index 00000000..90ff3e20 --- /dev/null +++ b/src/Giraffe/ResponseCaching.fs @@ -0,0 +1,117 @@ +[] +module Giraffe.ResponseCaching + +open System +open Microsoft.Extensions.Primitives +open Microsoft.Net.Http.Headers +open Microsoft.AspNetCore.Http +open Microsoft.AspNetCore.ResponseCaching + +/// **Description** +/// +/// Specifies the directive for the `Cache-Control` HTTP header: +/// +/// - `NoCache`: The resource should not be cached under any circumstances. +/// - `Public`: Any client and proxy may cache the resource for the given amount of time. +/// - `Private`: Only the end client may cache the resource for the given amount of time. +/// +type CacheDirective = + | NoCache + | Public of TimeSpan + | Private of TimeSpan + +let private noCacheHeader = CacheControlHeaderValue(NoCache = true, NoStore = true) + +let inline private cacheHeader isPublic duration = + CacheControlHeaderValue( + Public = isPublic, + MaxAge = Nullable duration) + +/// **Description** +/// +/// Enables (or suppresses) response caching by clients and proxy servers. +/// This http handler integrates with ASP.NET Core's response caching middleware. +/// +/// The `responseCaching` http handler will set the relevant HTTP response headers in order to enable response caching on the client, by proxies (if public) and by the ASP.NET Core middleware (if enabled). +/// +/// **Parameters** +/// +/// - `directive`: Specifies the cache directive to be set in the response's HTTP headers. Use `NoCache` to suppress caching altogether or use `Public`/`Private` to enable caching for everyone or clients only. +/// - `vary`: Optionally specify which HTTP headers have to match in order to return a cached response (e.g. `Accept` and/or `Accept-Encoding`). +/// - `varyByQueryKeys`: An optional list of query keys which will be used by the ASP.NET Core response caching middleware to vary (potentially) cached responses. If this parameter is used then the ASP.NET Core response caching middleware has to be enabled. For more information check the official [VaryByQueryKeys](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/middleware?view=aspnetcore-2.1#varybyquerykeys) documentation. +/// +/// **Output** +/// +/// A Giraffe `HttpHandler` function which can be composed into a bigger web application. +/// +let responseCaching (directive : CacheDirective) + (vary : string option) + (varyByQueryKeys : string array option) : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + + let tHeaders = ctx.Response.GetTypedHeaders() + let headers = ctx.Response.Headers + + match directive with + | NoCache -> + tHeaders.CacheControl <- noCacheHeader + headers.[ HeaderNames.Pragma ] <- StringValues [| "no-cache" |] + headers.[ HeaderNames.Expires ] <- StringValues [| "-1" |] + | Public duration -> tHeaders.CacheControl <- cacheHeader true duration + | Private duration -> tHeaders.CacheControl <- cacheHeader false duration + + if vary.IsSome then headers.[HeaderNames.Vary] <- StringValues [| vary.Value |] + + if varyByQueryKeys.IsSome then + let responseCachingFeature = ctx.Features.Get() + if isNotNull responseCachingFeature then + responseCachingFeature.VaryByQueryKeys <- varyByQueryKeys.Value + + next ctx + +/// **Description** +/// +/// Disables response caching by clients and proxy servers. +/// +/// **Output** +/// +/// A Giraffe `HttpHandler` function which can be composed into a bigger web application. +/// +let noResponseCaching : HttpHandler = responseCaching NoCache None None + +/// **Description** +/// +/// Enables response caching for clients and proxy servers. +/// This http handler integrates with ASP.NET Core's response caching middleware. +/// +/// The `responseCaching` http handler will set the relevant HTTP response headers in order to enable response caching on the client, by proxies and by the ASP.NET Core middleware (if enabled). +/// +/// **Parameters** +/// +/// - `seconds`: Specifies the duration (in seconds) for which the response may be cached. +/// - `vary`: Optionally specify which HTTP headers have to match in order to return a cached response (e.g. `Accept` and/or `Accept-Encoding`). +/// +/// **Output** +/// +/// A Giraffe `HttpHandler` function which can be composed into a bigger web application. +/// +let privateResponseCaching (seconds : int) (vary : string option) : HttpHandler = + responseCaching (Private (TimeSpan.FromSeconds(float seconds))) vary None + +/// **Description** +/// +/// Enables response caching for clients only. +/// +/// The `responseCaching` http handler will set the relevant HTTP response headers in order to enable response caching on the client only. +/// +/// **Parameters** +/// +/// - `seconds`: Specifies the duration (in seconds) for which the response may be cached. +/// - `vary`: Optionally specify which HTTP headers have to match in order to return a cached response (e.g. `Accept` and/or `Accept-Encoding`). +/// +/// **Output** +/// +/// A Giraffe `HttpHandler` function which can be composed into a bigger web application. +/// +let publicResponseCaching (seconds : int) (vary : string option) : HttpHandler = + responseCaching (Public (TimeSpan.FromSeconds(float seconds))) vary None \ No newline at end of file From 371c11ef5cb50ace606369bbde1ddfed8a0fcd41 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Mon, 17 Sep 2018 23:20:35 +0100 Subject: [PATCH 41/43] Extending docs and release notes Work in progress... --- DOCUMENTATION.md | 31 +++++++++++++++--- RELEASE_NOTES.md | 26 ++++++++++++--- src/Giraffe/Middleware.fs | 2 +- src/Giraffe/ResponseWriters.fs | 15 ++++----- tests/Giraffe.Tests/Helpers.fs | 40 ++++++++++-------------- tests/Giraffe.Tests/HttpHandlerTests.fs | 16 +++++----- tests/Giraffe.Tests/ModelBindingTests.fs | 8 ++--- tests/Giraffe.Tests/RoutingTests.fs | 2 -- 8 files changed, 86 insertions(+), 54 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index ac32beaf..612aae71 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -2207,7 +2207,7 @@ let someHandler (str : string) : HttpHandler = #### Writing JSON -The `WriteJsonAsync (dataObj : obj)` extension method and the `json (dataObj : obj)` http handler will both serialize an object to a JSON string and write the output to the response stream of the HTTP request. They will also set the `Content-Length`HTTP header and the `Content-Type` header to `application/json` in the response: +The `WriteJsonAsync<'T> (dataObj : 'T)` extension method and the `json<'T> (dataObj : 'T)` http handler will both serialize an object to a JSON string and write the output to the response stream of the HTTP request. They will also set the `Content-Length` HTTP header and the `Content-Type` header to `application/json` in the response: ```fsharp let someHandler (dataObj : obj) : HttpHandler = @@ -2226,6 +2226,8 @@ let someHandler (dataObj : obj) : HttpHandler = The underlying JSON serializer can be configured as a dependency during application startup (see [JSON](#json)). +The `WriteJsonChunkedAsync<'T> (dataObj : 'T)` extension method and the `jsonChunked (dataObj : 'T)` http handler write directly to th response stream of the HTTP request without extra buffering into a byte array. + #### Writing XML The `WriteXmlAsync (dataObj : obj)` extension method and the `xml (dataObj : obj)` http handler will both serialize an object to an XML string and write the output to the response stream of the HTTP request. They will also set the `Content-Length`HTTP header and the `Content-Type` header to `application/xml` in the response: @@ -2939,7 +2941,25 @@ Overall the Giraffe View Engine is extremely flexible and feature rich by nature By default Giraffe uses [Newtonsoft JSON.NET](https://www.newtonsoft.com/json) for (de-)serializing JSON content. An application can modify the default serializer by registering a new dependency which implements the `IJsonSerializer` interface during application startup. -Customizing Giraffe's JSON serialization can either happen via providing a custom object of `JsonSerializerSettings` when instantiating the default `NewtonsoftJsonSerializer` or swap in an entire different JSON library by creating a new class which implements the `IJsonSerializer` interface. +Customizing Giraffe's JSON serialization can either happen via providing a custom object of `JsonSerializerSettings` when instantiating the default `NewtonsoftJsonSerializer` or by swapping in an entire different JSON library by creating a new class which implements the `IJsonSerializer` interface. + +By default Giraffe offers two `IJsonSerializer` implementations out of the box: + +| Name | Description | Default | +| :--- | :---------- | :------ | +| `NewtonsoftJsonSerializer` | Uses `Newtonsoft.Json` aka JSON.Net for JSON (de-)serialization in Giraffe. It is the most downloaded library on NuGet, battle tested by millions of users and has great support for F# data types. Use this json serializer for maximum compatibility and easy adoption. | True | +| `Utf8JsonSerializer` | Uses `Utf8Json` for JSON (de-)serialization in Giraffe. This is the fastest JSON serializer written in .NET with huge extensibility points and native support for directly serializing JSON content to the HTTP response stream via chunked encoding. This serializer has been specifically crafted for maximum performance and should be used when that extra perf is important. | False | + +The `Utf8JsonSerializer` can be used instead of the `NewtonsoftJsonSerializer` by registering a new dependency of type `IJsonSerializer` during application configuration: + +```fsharp +let configureServices (services : IServiceCollection) = + // First register all default Giraffe dependencies + services.AddGiraffe() |> ignore + + // Now register Utf8JsonSerializer + this.AddSingleton(Utf8JsonSerializer(Utf8JsonSerializer.DefaultResolver)) |> ignore +``` #### Customizing JsonSerializerSettings @@ -2977,9 +2997,12 @@ You can change the entire underlying JSON serializer by creating a new class whi type CustomJsonSerializer() = interface IJsonSerializer with // Use different JSON library ... - member __.Serialize (o : obj) = // ... + member __.SerializeToString<'T> (x : 'T) = // ... + member __.SerializeToBytes<'T> (x : 'T) = // ... + member __.SerializeToStreamAsync<'T> (x : 'T) = // ... + member __.Deserialize<'T> (json : string) = // ... - member __.Deserialize<'T> (stream : Stream) = // ... + member __.Deserialize<'T> (bytes : byte[]) = // ... member __.DeserializeAsync<'T> (stream : Stream) = // ... ``` diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a5fd9ef5..1fd81741 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -5,20 +5,38 @@ Release Notes #### Breaking changes -- Changed the type `XmlNode` by removing the `RawText` and `EncodedText` union cases and replaced them by a single `Text` union case. The encoding is being done at an earlier stage now when calling one of the two helper functions `rawText` and `encodedText`. +- Changed the type `XmlNode` by removing the `RawText` and `EncodedText` union case and replaced both by a single `Text` union case. The HTML encoding (or not) is being done now when calling one of the two helper functions `rawText` and `encodedText`. -This change should not affect the majority of Giraffe users unless you were constructing your own `XmlNode` elements which were of type `RawText` or `EncodedText`. + - This change - even though theoretically a breaking change - should not affect the vast majority of Giraffe users unless you were constructing your own `XmlNode` elements which were of type `RawText` or `EncodedText` (which is extremely unlikely given that there's not much room for more nodes of these two types). -- Removed the `task {}` override in Giraffe which was forcing the `FSharp.Control.Tasks.V2.ContextInsensitive` version of the Task CE. This change has no effect on existing Giraffe applications other than that it might require an additional open statement of the aforementioned module in some places, but behaviour wise it will remain exactly the same (even if the context sensitive module gets opened, because there is no difference between context sensitive or insensitive in the context of an ASP.NET Core application). +- Removed the `task {}` override in Giraffe which was forcing the `FSharp.Control.Tasks.V2.ContextInsensitive` version of the Task CE. This change has no effect on the behaviour of `task` computation expressions in Giraffe. In the context of an ASP.NET Core web application there is not difference between `ContextSensitive` and `ContextInsensitive` which is why the override has been removed. The only breaking change which could affect an existing Giraffe web application is that in some places you will need to explicitly `open FSharp.Control.Tasks.V2.ContextInsensitive` where before it might have been sufficient to only `open Giraffe`. + +- Changed the members of the `IJsonSerializer` interface to accommodate new (de-)serialize methods for chunked encoding transfer. + + The new interface is the following: + + ```fsharp + type IJsonSerializer = + abstract member SerializeToString<'T> : 'T -> string + abstract member SerializeToBytes<'T> : 'T -> byte array + abstract member SerializeToStreamAsync<'T> : 'T -> Stream -> Task + + abstract member Deserialize<'T> : string -> 'T + abstract member Deserialize<'T> : byte[] -> 'T + abstract member DeserializeAsync<'T> : Stream -> Task<'T> + ``` #### Improvements -- Significant performance gains by changing the underlying way of how views are being composed by the `GriaffeViewEngine`. +- Significant performance improvements in the `GiraffeViewEngine` by changing the underlying composition of views from simple string concatenation to using a `StringBuilder` object. #### New features - Support for short GUIDs and short IDs (aka YouTube IDs) [in route arguments](https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#routef) and [query string parameters](https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#short-guids-and-short-ids). - Enabled [SourceLink](https://github.com/dotnet/sourcelink/) support for Giraffe source code (thanks [Cameron Taggart](https://github.com/ctaggart))! For more information check out [Adding SourceLink to your .NET Core Library](https://carlos.mendible.com/2018/08/25/adding-sourcelink-to-your-net-core-library/). +- Added a new JSON serializer called `Utf8JsonSerializer`. This type uses the [Utf8 JSON serializer library](https://github.com/neuecc/Utf8Json/), which is currently the fastest JSON serializer for .NET. `NewtonsoftJsonSerializer` is still the default JSON serializer in Giraffe (for stability and backwards compatibility), but `Utf8JsonSerializer` can be swapped in via [ASP.NET Core's dependency injection API](https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#json). The new `Utf8JsonnSerializer` is significantly faster (especially when sending chunked responses) than `NewtonsoftJsonSerializer`. +- Added a new `HttpContext` extension method for chunked JSON transfers: `WriteJsonChunkedAsync<'T> (dataObj : 'T)`. This new `HttpContext` method can write content directly to the HTTP response stream without buffering into a byte array first. +- Added a new `jsonChunked` http handler. This handler is the equivalent http handler version of the `WriteJsonChunkedAsync` extension method. ## 2.0.1 diff --git a/src/Giraffe/Middleware.fs b/src/Giraffe/Middleware.fs index 4a1b39f6..56237b31 100644 --- a/src/Giraffe/Middleware.fs +++ b/src/Giraffe/Middleware.fs @@ -122,7 +122,7 @@ type IServiceCollection with /// Returns an `IServiceCollection` builder object. /// member this.AddGiraffe() = - this.TryAddSingleton(Utf8JsonSerializer(Utf8Json.JsonSerializer.DefaultResolver)) + this.TryAddSingleton(NewtonsoftJsonSerializer(NewtonsoftJsonSerializer.DefaultSettings)) this.TryAddSingleton(DefaultXmlSerializer(DefaultXmlSerializer.DefaultSettings)) this.TryAddSingleton() this \ No newline at end of file diff --git a/src/Giraffe/ResponseWriters.fs b/src/Giraffe/ResponseWriters.fs index b6d26ad5..d898ba9d 100644 --- a/src/Giraffe/ResponseWriters.fs +++ b/src/Giraffe/ResponseWriters.fs @@ -102,13 +102,14 @@ type HttpContext with /// /// Task of `Some HttpContext` after writing to the body of the response. /// - member this.WriteJsonChunkedAsync<'T> (dataObj : 'T) = task { - this.SetContentType "application/json" - this.SetHttpHeader "Transfer-Encoding" "chunked" - let serializer = this.GetJsonSerializer() - do! serializer.SerializeToStreamAsync dataObj this.Response.Body - return Some this - } + member this.WriteJsonChunkedAsync<'T> (dataObj : 'T) = + task { + this.SetContentType "application/json" + this.SetHttpHeader "Transfer-Encoding" "chunked" + let serializer = this.GetJsonSerializer() + do! serializer.SerializeToStreamAsync dataObj this.Response.Body + return Some this + } /// **Description** /// diff --git a/tests/Giraffe.Tests/Helpers.fs b/tests/Giraffe.Tests/Helpers.fs index 39bd5450..d1f171d5 100644 --- a/tests/Giraffe.Tests/Helpers.fs +++ b/tests/Giraffe.Tests/Helpers.fs @@ -25,16 +25,14 @@ open Utf8Json // Common functions // --------------------------------- -let toTheoryData xs = +let toTheoryData xs = let data = new TheoryData<_>() - for x in xs do - data.Add x + for x in xs do data.Add x data -let toTheoryData2 xs = +let toTheoryData2 xs = let data = new TheoryData<_,_>() - for (a,b) in xs do - data.Add(a,b) + for (a,b) in xs do data.Add(a,b) data let waitForDebuggerToAttach() = @@ -110,10 +108,9 @@ let createHost (configureApp : 'Tuple -> IApplicationBuilder -> unit) .Configure(Action (configureApp args)) .ConfigureServices(Action configureServices) - -type MockJsonSettings = - | Newtonsoft of JsonSerializerSettings option - | Utf8 of IJsonFormatterResolver option +type MockJsonSettings = + | Newtonsoft of JsonSerializerSettings option + | Utf8 of IJsonFormatterResolver option let mockJson (ctx : HttpContext) (settings : MockJsonSettings) = @@ -121,23 +118,20 @@ let mockJson (ctx : HttpContext) (settings : MockJsonSettings) = | Newtonsoft settings -> let jsonSettings = defaultArg settings NewtonsoftJsonSerializer.DefaultSettings - ctx.RequestServices .GetService(typeof) .Returns(new NewtonsoftJsonSerializer(jsonSettings)) |> ignore | Utf8 settings -> - - let resolver = + let resolver = defaultArg settings Utf8JsonSerializer.DefaultResolver - ctx.RequestServices .GetService(typeof) .Returns(new Utf8JsonSerializer(resolver)) |> ignore - -type JsonSerializersData = + +type JsonSerializersData = static member DefaultSettings = [ Utf8 None; @@ -146,14 +140,14 @@ type JsonSerializersData = static member DefaultData = JsonSerializersData.DefaultSettings |> toTheoryData - static member PreserveCaseSettings = + static member PreserveCaseSettings = [ Utf8 (Some Utf8Json.Resolvers.StandardResolver.Default) - Newtonsoft (Some <| new JsonSerializerSettings()) - ] - + Newtonsoft (Some (new JsonSerializerSettings())) + ] + static member PreserveCaseData = JsonSerializersData.PreserveCaseSettings |> toTheoryData - + let mockXml (ctx : HttpContext) = ctx.RequestServices .GetService(typeof) @@ -257,6 +251,4 @@ let shouldBeEmpty (bytes : byte[]) = Assert.True(bytes.Length.Equals 0) let shouldEqual expected actual = - Assert.Equal(expected, actual) - - + Assert.Equal(expected, actual) \ No newline at end of file diff --git a/tests/Giraffe.Tests/HttpHandlerTests.fs b/tests/Giraffe.Tests/HttpHandlerTests.fs index cb3c4f9d..714fc55e 100644 --- a/tests/Giraffe.Tests/HttpHandlerTests.fs +++ b/tests/Giraffe.Tests/HttpHandlerTests.fs @@ -45,7 +45,7 @@ type Person = // --------------------------------- [] -[)>] +[)>] let ``GET "/json" returns json object`` (settings) = let ctx = Substitute.For() mockJson ctx settings @@ -70,7 +70,7 @@ let ``GET "/json" returns json object`` (settings) = } [] -[)>] +[)>] let ``GET "/json" with custom json settings returns json object`` (settings) = let ctx = Substitute.For() @@ -95,12 +95,12 @@ let ``GET "/json" with custom json settings returns json object`` (settings) = | Some ctx -> Assert.Equal(expected, getBody ctx) } -let DefaultMocksWithSize = +let DefaultMocksWithSize = [ - let ``powers of two`` = [1..10] |> List.map (pown 2) + let ``powers of two`` = [ 1..10 ] |> List.map (pown 2) for size in ``powers of two`` do for setting in JsonSerializersData.DefaultSettings do - yield size ,setting + yield size, setting ] |> toTheoryData2 [] @@ -119,7 +119,7 @@ let ``GET "/jsonChunked" returns json object`` (size: int, settings) = ctx.Request.Path.ReturnsForAnyArgs (PathString("/jsonChunked")) |> ignore ctx.Response.Body <- new MemoryStream() - let expected = + let expected = let o = "{\"foo\":\"john\",\"bar\":\"doe\",\"age\":30}" let os = Array.replicate size o |> String.concat "," "[" + os + "]" @@ -132,7 +132,7 @@ let ``GET "/jsonChunked" returns json object`` (size: int, settings) = | Some ctx -> Assert.Equal(expected, getBody ctx) } -let CamelCasedMocksWithSize = +let CamelCasedMocksWithSize = [ let ``powers of two`` = [1..10] |> List.map (pown 2) for size in ``powers of two`` do @@ -156,7 +156,7 @@ let ``GET "/jsonChunked" with custom json settings returns json object`` (size: ctx.Request.Path.ReturnsForAnyArgs (PathString("/jsonChunked")) |> ignore ctx.Response.Body <- new MemoryStream() - let expected = + let expected = let o = "{\"Foo\":\"john\",\"Bar\":\"doe\",\"Age\":30}" let os = Array.replicate size o |> String.concat "," "[" + os + "]" diff --git a/tests/Giraffe.Tests/ModelBindingTests.fs b/tests/Giraffe.Tests/ModelBindingTests.fs index 9af52bc8..2b1ae4a7 100644 --- a/tests/Giraffe.Tests/ModelBindingTests.fs +++ b/tests/Giraffe.Tests/ModelBindingTests.fs @@ -548,7 +548,7 @@ let ``tryBindQuery with complete data but baldy formated list items`` () = // --------------------------------- [] -[)>] +[)>] let ``BindJsonAsync test`` (settings) = let ctx = Substitute.For() mockJson ctx settings @@ -771,7 +771,7 @@ let ``BindQueryString with nullable property test`` () = } [] -[)>] +[)>] let ``BindModelAsync with JSON content returns correct result`` (settings) = let ctx = Substitute.For() mockJson ctx settings @@ -810,7 +810,7 @@ let ``BindModelAsync with JSON content returns correct result`` (settings) = } [] -[)>] +[)>] let ``BindModelAsync with JSON content that uses custom serialization settings returns correct result`` (settings) = let ctx = Substitute.For() mockJson ctx settings @@ -966,7 +966,7 @@ let ``BindModelAsync with culture aware form content returns correct result`` () } [] -[)>] +[)>] let ``BindModelAsync with JSON content and a specific charset returns correct result`` (settings) = let ctx = Substitute.For() mockJson ctx settings diff --git a/tests/Giraffe.Tests/RoutingTests.fs b/tests/Giraffe.Tests/RoutingTests.fs index fbd32e66..5e691232 100644 --- a/tests/Giraffe.Tests/RoutingTests.fs +++ b/tests/Giraffe.Tests/RoutingTests.fs @@ -86,8 +86,6 @@ let ``route: GET "/FOO" returns 404 "Not found"`` () = // routeCi Tests // --------------------------------- - - [] let ``GET "/JSON" returns "BaR"`` () = let ctx = Substitute.For() From 63f47381cffe132a2cdbd21efb982474009d9ad9 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Tue, 18 Sep 2018 06:14:37 +0100 Subject: [PATCH 42/43] Finished docs and release notes for JSON changes Finished the docs and release notes about the new JSON changes. --- DOCUMENTATION.md | 25 ++++++++++++++++++++----- RELEASE_NOTES.md | 2 +- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 612aae71..ec249ad4 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -2210,23 +2210,38 @@ let someHandler (str : string) : HttpHandler = The `WriteJsonAsync<'T> (dataObj : 'T)` extension method and the `json<'T> (dataObj : 'T)` http handler will both serialize an object to a JSON string and write the output to the response stream of the HTTP request. They will also set the `Content-Length` HTTP header and the `Content-Type` header to `application/json` in the response: ```fsharp -let someHandler (dataObj : obj) : HttpHandler = +let someHandler (animal : Animal) : HttpHandler = fun (next : HttpFunc) (ctx : HttpContext) -> task { // Do stuff - return! ctx.WriteJsonAsync dataObj + return! ctx.WriteJsonAsync animal } // or... -let someHandler (dataObj : obj) : HttpHandler = +let someHandler (animal : Animal) : HttpHandler = // Do stuff - json dataObj + json animal ``` The underlying JSON serializer can be configured as a dependency during application startup (see [JSON](#json)). -The `WriteJsonChunkedAsync<'T> (dataObj : 'T)` extension method and the `jsonChunked (dataObj : 'T)` http handler write directly to th response stream of the HTTP request without extra buffering into a byte array. +The `WriteJsonChunkedAsync<'T> (dataObj : 'T)` extension method and the `jsonChunked (dataObj : 'T)` http handler write directly to th response stream of the HTTP request without extra buffering into a byte array. They will not set a `Content-Length` header and instead set the `Transfer-Encoding: chunked` header and `Content-Type: application/json`: + +```fsharp +let someHandler (person : Person) : HttpHandler = + fun (next : HttpFunc) (ctx : HttpContext) -> + task { + // Do stuff + return! ctx.WriteJsonChunkedAsync person + } + +// or... + +let someHandler (person : Person) : HttpHandler = + // Do stuff + jsonChunked person +``` #### Writing XML diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1fd81741..068f52b7 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -35,7 +35,7 @@ Release Notes - Support for short GUIDs and short IDs (aka YouTube IDs) [in route arguments](https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#routef) and [query string parameters](https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#short-guids-and-short-ids). - Enabled [SourceLink](https://github.com/dotnet/sourcelink/) support for Giraffe source code (thanks [Cameron Taggart](https://github.com/ctaggart))! For more information check out [Adding SourceLink to your .NET Core Library](https://carlos.mendible.com/2018/08/25/adding-sourcelink-to-your-net-core-library/). - Added a new JSON serializer called `Utf8JsonSerializer`. This type uses the [Utf8 JSON serializer library](https://github.com/neuecc/Utf8Json/), which is currently the fastest JSON serializer for .NET. `NewtonsoftJsonSerializer` is still the default JSON serializer in Giraffe (for stability and backwards compatibility), but `Utf8JsonSerializer` can be swapped in via [ASP.NET Core's dependency injection API](https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#json). The new `Utf8JsonnSerializer` is significantly faster (especially when sending chunked responses) than `NewtonsoftJsonSerializer`. -- Added a new `HttpContext` extension method for chunked JSON transfers: `WriteJsonChunkedAsync<'T> (dataObj : 'T)`. This new `HttpContext` method can write content directly to the HTTP response stream without buffering into a byte array first. +- Added a new `HttpContext` extension method for chunked JSON transfers: `WriteJsonChunkedAsync<'T> (dataObj : 'T)`. This new `HttpContext` method can write content directly to the HTTP response stream without buffering into a byte array first (see [Writing JSON](https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#writing-json)). - Added a new `jsonChunked` http handler. This handler is the equivalent http handler version of the `WriteJsonChunkedAsync` extension method. ## 2.0.1 From 60fcfb8a33a758387913d69074464f26e2550d91 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Tue, 18 Sep 2018 07:47:31 +0100 Subject: [PATCH 43/43] Added docs and release notes for caching support Finished the docs and release notes. --- DOCUMENTATION.md | 104 ++++++++++++++++++ RELEASE_NOTES.md | 3 +- samples/SampleApp/SampleApp/Program.fs | 23 +++- .../Giraffe.Benchmarks.fsproj | 2 - tests/Giraffe.Benchmarks/Program.fs | 1 - 5 files changed, 126 insertions(+), 7 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index ec249ad4..43f3cefd 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -33,6 +33,8 @@ An in depth functional reference to all of Giraffe's default features. - [Content Negotiation](#content-negotiation) - [Streaming](#streaming) - [Redirection](#redirection) + - [Response Caching](#response-caching) + - [Response Compression](#response-compression) - [Giraffe View Engine](#giraffe-view-engine) - [HTML Elements and Attributes](#html-elements-and-attributes) - [Text Content](#text-content) @@ -2567,6 +2569,108 @@ let webApp = Please note that if the `permanent` flag is set to `true` then the Giraffe web application will send a `301` HTTP status code to browsers which will tell them that the redirection is permanent. This often leads to browsers cache the information and not hit the deprecated URL a second time any more. If this is not desired then please set `permanent` to `false` in order to guarantee that browsers will continue hitting the old URL before redirecting to the (temporary) new one. +### Response Caching + +ASP.NET Core comes with a standard [Response Caching Middleware](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/middleware?view=aspnetcore-2.1) which works out of the box with Giraffe. + +If you are not already using one of the two ASP.NET Core meta packages (`Microsoft.AspNetCore.App` or `Microsoft.AspNetCore.All`) then you will have to add an additional reference to the [Microsoft.AspNetCore.ResponseCaching](https://www.nuget.org/packages/Microsoft.AspNetCore.ResponseCaching/) NuGet package. + +After adding the NuGet package you need to register the response caching middleware inside your application's startup code before registering Giraffe: + +```fsharp +let configureServices (services : IServiceCollection) = + services + .AddResponseCaching() // <-- Here the order doesn't matter + .AddGiraffe() // This is just registering dependencies + |> ignore + +let configureApp (app : IApplicationBuilder) = + app.UseGiraffeErrorHandler(errorHandler) + .UseStaticFiles() // Optional if you use static files + .UseAuthentication() // Optional if you use authentication + .UseResponseCaching() // <-- Before UseGiraffe webApp + .UseGiraffe webApp +``` + +After setting up the [ASP.NET Core response caching middleware](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/middleware?view=aspnetcore-2.1#configuration) you can use Giraffe's response caching http handlers to add response caching to your routes: + +```fsharp +// A test handler which generates a new GUID on every request +let generateGuidHandler : HttpHandler = + warbler (fun _ -> text (Guid.NewGuid().ToString())) + +let webApp = + GET >=> choose [ + route "/route1" >=> publicResponseCaching 30 None >=> generateGuidHandler + route "/route2" >=> noResponseCaching >=> generateGuidHandler + ] +``` + +Requests to `/route1` can be cached for up to 30 seconds whilst requests to `/route2` have response caching completely disabled. + +*Note: if you test the above code with [Postman](https://www.getpostman.com/) then make sure you [disable the No-Cache feature](https://www.getpostman.com/docs/v6/postman/launching_postman/settings) in Postman in order to test the correct caching behaviour.* + +Giraffe offers in total 4 http handlers which can be used to configure response caching for an endpoint. + +In the above example we used the `noResponseCaching` http handler to completely disable response caching on the client and on any proxy server. The `noResponseCaching` http handler will send the following HTTP headers in the response: + +``` +Cache-Control: no-store, no-cache +Pragma: no-cache +Expires: -1 +``` + +The `publicResponseCaching` or `privateResponseCaching` http handlers will enable response caching on the client and/or on proxy servers. The +`publicResponseCaching` http handler will set the `Cache-Control` directive to `public`, which means that not only the client is allowed to cache a response for the given cache duration, but also any intermediary proxy server as well as the ASP.NET Core middleware. This is useful for HTTP GET/HEAD endpoints which do not hold any user specific data, authentication data or any cookies and where the response data doesn't change frequently. + +The `privateResponseCaching` http handler sets the `Cache-Control` directive to `private` which means that only the end client is allowed to store the response for the given cache duration. Proxy servers and the ASP.NET Core response caching middleware must not cache the response. + +Both http handlers require the cache duration in seconds and an optional `vary` parameter: + +```fsharp +// Cache for 10 seconds without any vary headers +publicResponseCaching 10 None + +// Cache for 30 seconds with Accept and Accept-Encoding as vary headers +publicResponseCaching 30 (Some "Accept, Accept-Encoding") +``` + +The `vary` parameter specifies which HTTP request headers must be respected to vary the cached response. For example if an endpoint returns a different response (`Content-Type`) based on the client's `Accept` header (= [content negotiation](#content-negotiation)) then the `Accept` header must also be considered when returning a response from the cache. The same applies if the web server has response compression enabled. If a response varies based on the client's accepted compression algorithm then the cache must also respect the client's `Accept-Encoding` HTTP header when serving a response from the cache. + +#### VaryByQueryKeys + +The ASP.NET Core response caching middleware offers one more additional feature which is not part of the response's HTTP headers. By default, if a route is cachable then the middleware will try to returnn a cached response even if the query parameters were different. + +For example if a request to `/foo/bar` has been cached, then the cached version will also be returned if a request is made to `/foo/bar?query1=a` or `/foo/bar?query1=a&query2=b`. + +Sometimes this is not desired and the `VaryByQueryKeys` feature lets the [middleware vary its cached responses based on a request's query keys](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/middleware?view=aspnetcore-2.1#varybyquerykeys). + +The generic `responseCaching` http handler is the most basic response caching handler which can be used to configure custom response caching handlers as well as make use of the `VaryByQueryKeys` feature: + +```fsharp +responseCaching + (Public (TimeSpan.FromSeconds (float 5))) + (Some "Accept, Accept-Encoding") + (Some [| "query1"; "query2" |]) +``` + +The first parameter is of type `CacheDirective` which is defines as following: + +```fsharp +type CacheDirective = + | NoCache + | Public of TimeSpan + | Private of TimeSpan +``` + +The second parameter is an `string option` which defines the `vary` parameter. + +The third and last parameter is a `string list option` which defines an optional list of query parameter values which must be used to vary a cached response by the ASP.NET Core response caching middleware. Please be aware that this feature only applies to the ASP.NET Core response caching middleware and will not be respected by any intermediate proxy servers. + +### Response Compression + +ASP.NET Core has its own [Response Compression Middleware](https://docs.microsoft.com/en-us/aspnet/core/performance/response-compression?view=aspnetcore-2.1&tabs=aspnetcore2x) which works out of the box with Giraffe. There's no additional functionality or http handlers required in order to make it work with Giraffe web applications. + ## Giraffe View Engine Giraffe has its own functional view engine which can be used to build rich UIs for web applications. The single biggest and best contrast to other view engines (e.g. Razor, Liquid, etc.) is that the Giraffe View Engine is entirely functional written in normal (and compiled) F# code. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 068f52b7..5f35699f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -37,6 +37,7 @@ Release Notes - Added a new JSON serializer called `Utf8JsonSerializer`. This type uses the [Utf8 JSON serializer library](https://github.com/neuecc/Utf8Json/), which is currently the fastest JSON serializer for .NET. `NewtonsoftJsonSerializer` is still the default JSON serializer in Giraffe (for stability and backwards compatibility), but `Utf8JsonSerializer` can be swapped in via [ASP.NET Core's dependency injection API](https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#json). The new `Utf8JsonnSerializer` is significantly faster (especially when sending chunked responses) than `NewtonsoftJsonSerializer`. - Added a new `HttpContext` extension method for chunked JSON transfers: `WriteJsonChunkedAsync<'T> (dataObj : 'T)`. This new `HttpContext` method can write content directly to the HTTP response stream without buffering into a byte array first (see [Writing JSON](https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#writing-json)). - Added a new `jsonChunked` http handler. This handler is the equivalent http handler version of the `WriteJsonChunkedAsync` extension method. +- Added first class support for [ASP.NET Core's response caching](https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#response-caching) feature. ## 2.0.1 @@ -58,7 +59,7 @@ Changed the `task {}` CE to load from `FSharp.Control.Tasks.V2.ContextInsensitiv - Enabled `return!` for `opt { }` computation expressions. - Added `blockquote`, `_integrity` and `_scoped` to the `GiraffeViewEngine`. - Added attributes for mouse, keyboard, touch, drag & drop, focus, input and mouse wheel events to the `GiraffeViewEngine`. -- Added new accessibility attributes to the `GriaffeViewEngine`. These can be used after opening the `Giraffe.GiraffeViewEngine.Accessibility` module. +- Added new accessibility attributes to the `GiraffeViewEngine`. These can be used after opening the `Giraffe.GiraffeViewEngine.Accessibility` module. - Added a new `Successful.NO_CONTENT` http handler which can be used to return a HTTP 204 response. - Added more structured logging around the Giraffe middleware. diff --git a/samples/SampleApp/SampleApp/Program.fs b/samples/SampleApp/SampleApp/Program.fs index 36fb7321..7dfe4607 100644 --- a/samples/SampleApp/SampleApp/Program.fs +++ b/samples/SampleApp/SampleApp/Program.fs @@ -98,6 +98,22 @@ let fileUploadHandler2 = |> text) next ctx } +let cacheHandler1 : HttpHandler = + publicResponseCaching 30 None + >=> warbler (fun _ -> + text (Guid.NewGuid().ToString())) + +let cacheHandler2 : HttpHandler = + responseCaching + (Public (TimeSpan.FromSeconds (float 30))) + None + (Some [| "key1"; "key2" |]) + >=> warbler (fun _ -> + text (Guid.NewGuid().ToString())) + +let cacheHandler3 : HttpHandler = + noResponseCaching >=> warbler (fun _ -> text (Guid.NewGuid().ToString())) + let time() = System.DateTime.Now.ToString() [] @@ -133,8 +149,9 @@ let webApp = route "/configured" >=> configuredHandler route "/upload" >=> fileUploadHandler route "/upload2" >=> fileUploadHandler2 - route "/cache/1" >=> publicResponseCaching 30 None >=> warbler (fun _ -> text (Guid.NewGuid().ToString())) - route "/cache/2" >=> noResponseCaching >=> warbler (fun _ -> text (Guid.NewGuid().ToString())) + route "/cache/1" >=> cacheHandler1 + route "/cache/2" >=> cacheHandler2 + route "/cache/3" >=> cacheHandler3 ] route "/car" >=> bindModel None json route "/car2" >=> tryBindQuery parsingErrorHandler None (validateModel xml) @@ -153,9 +170,9 @@ let cookieAuth (o : CookieAuthenticationOptions) = let configureApp (app : IApplicationBuilder) = app.UseGiraffeErrorHandler(errorHandler) - .UseResponseCaching() .UseStaticFiles() .UseAuthentication() + .UseResponseCaching() .UseGiraffe webApp let configureServices (services : IServiceCollection) = diff --git a/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj b/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj index 82677e09..ecc4376e 100644 --- a/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj +++ b/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj @@ -20,6 +20,4 @@ - - \ No newline at end of file diff --git a/tests/Giraffe.Benchmarks/Program.fs b/tests/Giraffe.Benchmarks/Program.fs index c2b47162..3ffa3916 100644 --- a/tests/Giraffe.Benchmarks/Program.fs +++ b/tests/Giraffe.Benchmarks/Program.fs @@ -66,7 +66,6 @@ type HtmlUtf8Benchmark() = ArrayPool.Shared.Return(chars) stringBuilder.Clear() - [] let main args = let asm = typeof.Assembly