From c0050b138fc10cfadb3c0815c6c44d5822e0d5f1 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Mon, 13 Apr 2020 17:46:13 +0100 Subject: [PATCH 01/32] Upped NuGet dependencies and Copyright date --- .../GoogleAuthApp/GoogleAuthApp.fsproj | 2 +- .../IdentityApp/IdentityApp/IdentityApp.fsproj | 4 ++-- samples/JwtApp/JwtApp/JwtApp.fsproj | 2 +- .../SampleApp.Tests/SampleApp.Tests.fsproj | 8 ++++---- src/Giraffe/Giraffe.fsproj | 12 ++++++------ .../Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj | 2 +- tests/Giraffe.Tests/Giraffe.Tests.fsproj | 16 ++++++++-------- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/samples/GoogleAuthApp/GoogleAuthApp/GoogleAuthApp.fsproj b/samples/GoogleAuthApp/GoogleAuthApp/GoogleAuthApp.fsproj index c5324421..6baaea1b 100644 --- a/samples/GoogleAuthApp/GoogleAuthApp/GoogleAuthApp.fsproj +++ b/samples/GoogleAuthApp/GoogleAuthApp/GoogleAuthApp.fsproj @@ -8,7 +8,7 @@ - + diff --git a/samples/IdentityApp/IdentityApp/IdentityApp.fsproj b/samples/IdentityApp/IdentityApp/IdentityApp.fsproj index 745c4ec0..f54d0a23 100644 --- a/samples/IdentityApp/IdentityApp/IdentityApp.fsproj +++ b/samples/IdentityApp/IdentityApp/IdentityApp.fsproj @@ -8,8 +8,8 @@ - - + + diff --git a/samples/JwtApp/JwtApp/JwtApp.fsproj b/samples/JwtApp/JwtApp/JwtApp.fsproj index 8246424e..59060b22 100644 --- a/samples/JwtApp/JwtApp/JwtApp.fsproj +++ b/samples/JwtApp/JwtApp/JwtApp.fsproj @@ -7,7 +7,7 @@ - + diff --git a/samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj b/samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj index d6110914..912d180f 100644 --- a/samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj +++ b/samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj @@ -7,10 +7,10 @@ - - - - + + + + diff --git a/src/Giraffe/Giraffe.fsproj b/src/Giraffe/Giraffe.fsproj index 7a71f5f4..ccad0cce 100644 --- a/src/Giraffe/Giraffe.fsproj +++ b/src/Giraffe/Giraffe.fsproj @@ -4,7 +4,7 @@ Giraffe 4.1.0 A native functional ASP.NET Core web framework for F# developers. - Copyright 2018 Dustin Moris Gorski + Copyright 2020 Dustin Moris Gorski Dustin Moris Gorski and contributors en-GB @@ -56,12 +56,12 @@ - - - + + + - - + + diff --git a/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj b/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj index e40b045a..81e0047f 100644 --- a/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj +++ b/tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj @@ -14,7 +14,7 @@ - + diff --git a/tests/Giraffe.Tests/Giraffe.Tests.fsproj b/tests/Giraffe.Tests/Giraffe.Tests.fsproj index 768b557e..c963c51b 100644 --- a/tests/Giraffe.Tests/Giraffe.Tests.fsproj +++ b/tests/Giraffe.Tests/Giraffe.Tests.fsproj @@ -41,17 +41,17 @@ - - - - - - - + + + + + + + - + From be2d3b2d212867143eb3e029303aa1e4928000b5 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Mon, 13 Apr 2020 18:11:20 +0100 Subject: [PATCH 02/32] Removed sample applications Sample applications moved into their own repository. This is in preparation to separate the GiraffeViewEngine into its own repository. --- Giraffe.sln | 89 ------ README.md | 46 ++- build.ps1 | 25 -- jmeter-load-test.jmx | 103 ------- .../GoogleAuthApp/GoogleAuthApp.fsproj | 23 -- .../GoogleAuthApp/HttpsConfig.fs | 94 ------- .../GoogleAuthApp/GoogleAuthApp/Program.fs | 192 ------------- .../IdentityApp/IdentityApp.fsproj | 23 -- samples/IdentityApp/IdentityApp/Program.fs | 266 ------------------ samples/JwtApp/JwtApp/JwtApp.fsproj | 21 -- samples/JwtApp/JwtApp/Program.fs | 106 ------- .../SampleApp.Tests/SampleApp.Tests.fsproj | 24 -- samples/SampleApp/SampleApp.Tests/Tests.fs | 132 --------- samples/SampleApp/SampleApp/HtmlViews.fs | 24 -- samples/SampleApp/SampleApp/Models.fs | 7 - samples/SampleApp/SampleApp/Program.fs | 203 ------------- samples/SampleApp/SampleApp/SampleApp.fsproj | 19 -- 17 files changed, 16 insertions(+), 1381 deletions(-) delete mode 100644 jmeter-load-test.jmx delete mode 100644 samples/GoogleAuthApp/GoogleAuthApp/GoogleAuthApp.fsproj delete mode 100644 samples/GoogleAuthApp/GoogleAuthApp/HttpsConfig.fs delete mode 100644 samples/GoogleAuthApp/GoogleAuthApp/Program.fs delete mode 100644 samples/IdentityApp/IdentityApp/IdentityApp.fsproj delete mode 100644 samples/IdentityApp/IdentityApp/Program.fs delete mode 100644 samples/JwtApp/JwtApp/JwtApp.fsproj delete mode 100644 samples/JwtApp/JwtApp/Program.fs delete mode 100644 samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj delete mode 100644 samples/SampleApp/SampleApp.Tests/Tests.fs delete mode 100644 samples/SampleApp/SampleApp/HtmlViews.fs delete mode 100644 samples/SampleApp/SampleApp/Models.fs delete mode 100644 samples/SampleApp/SampleApp/Program.fs delete mode 100644 samples/SampleApp/SampleApp/SampleApp.fsproj diff --git a/Giraffe.sln b/Giraffe.sln index 82b0b7cd..30ca8045 100644 --- a/Giraffe.sln +++ b/Giraffe.sln @@ -11,8 +11,6 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Giraffe", "src\Giraffe\Gira EndProject 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 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_misc", "_misc", "{CE10552A-F0F8-4C51-9CCF-4B0F160351E8}" ProjectSection(SolutionItems) = preProject giraffe-64x64.png = giraffe-64x64.png @@ -37,24 +35,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_build", "_build", "{8F5B79 NuGet.config = NuGet.config EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SampleApp", "SampleApp", "{E3500770-59F9-4526-9FFA-73E725C0008F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "JwtApp", "JwtApp", "{8AC0E934-6177-4D6D-9520-6E0931E527B9}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IdentityApp", "IdentityApp", "{3EE9B6D3-7ED6-43EE-9237-39E069042C65}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GoogleAuthApp", "GoogleAuthApp", "{53C796EB-20D2-4AF9-AAB9-130150005E21}" -EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "GoogleAuthApp", "samples\GoogleAuthApp\GoogleAuthApp\GoogleAuthApp.fsproj", "{FE396475-56EA-48AC-87B8-97EF6A66612F}" -EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "IdentityApp", "samples\IdentityApp\IdentityApp\IdentityApp.fsproj", "{3AAA2ECF-350F-4574-925F-21A909F41F42}" -EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "JwtApp", "samples\JwtApp\JwtApp\JwtApp.fsproj", "{BCD0E9C4-62AB-45B2-8362-A7AD1E4C03A7}" -EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "SampleApp", "samples\SampleApp\SampleApp\SampleApp.fsproj", "{61E7381C-C021-4048-A6F3-542E190F0D48}" -EndProject -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 @@ -91,66 +71,6 @@ Global {2AF14B8E-56FF-4E54-99DA-C530D573814D}.Release|x64.Build.0 = Release|Any CPU {2AF14B8E-56FF-4E54-99DA-C530D573814D}.Release|x86.ActiveCfg = Release|Any CPU {2AF14B8E-56FF-4E54-99DA-C530D573814D}.Release|x86.Build.0 = Release|Any CPU - {FE396475-56EA-48AC-87B8-97EF6A66612F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FE396475-56EA-48AC-87B8-97EF6A66612F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FE396475-56EA-48AC-87B8-97EF6A66612F}.Debug|x64.ActiveCfg = Debug|Any CPU - {FE396475-56EA-48AC-87B8-97EF6A66612F}.Debug|x64.Build.0 = Debug|Any CPU - {FE396475-56EA-48AC-87B8-97EF6A66612F}.Debug|x86.ActiveCfg = Debug|Any CPU - {FE396475-56EA-48AC-87B8-97EF6A66612F}.Debug|x86.Build.0 = Debug|Any CPU - {FE396475-56EA-48AC-87B8-97EF6A66612F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FE396475-56EA-48AC-87B8-97EF6A66612F}.Release|Any CPU.Build.0 = Release|Any CPU - {FE396475-56EA-48AC-87B8-97EF6A66612F}.Release|x64.ActiveCfg = Release|Any CPU - {FE396475-56EA-48AC-87B8-97EF6A66612F}.Release|x64.Build.0 = Release|Any CPU - {FE396475-56EA-48AC-87B8-97EF6A66612F}.Release|x86.ActiveCfg = Release|Any CPU - {FE396475-56EA-48AC-87B8-97EF6A66612F}.Release|x86.Build.0 = Release|Any CPU - {3AAA2ECF-350F-4574-925F-21A909F41F42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3AAA2ECF-350F-4574-925F-21A909F41F42}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3AAA2ECF-350F-4574-925F-21A909F41F42}.Debug|x64.ActiveCfg = Debug|Any CPU - {3AAA2ECF-350F-4574-925F-21A909F41F42}.Debug|x64.Build.0 = Debug|Any CPU - {3AAA2ECF-350F-4574-925F-21A909F41F42}.Debug|x86.ActiveCfg = Debug|Any CPU - {3AAA2ECF-350F-4574-925F-21A909F41F42}.Debug|x86.Build.0 = Debug|Any CPU - {3AAA2ECF-350F-4574-925F-21A909F41F42}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3AAA2ECF-350F-4574-925F-21A909F41F42}.Release|Any CPU.Build.0 = Release|Any CPU - {3AAA2ECF-350F-4574-925F-21A909F41F42}.Release|x64.ActiveCfg = Release|Any CPU - {3AAA2ECF-350F-4574-925F-21A909F41F42}.Release|x64.Build.0 = Release|Any CPU - {3AAA2ECF-350F-4574-925F-21A909F41F42}.Release|x86.ActiveCfg = Release|Any CPU - {3AAA2ECF-350F-4574-925F-21A909F41F42}.Release|x86.Build.0 = Release|Any CPU - {BCD0E9C4-62AB-45B2-8362-A7AD1E4C03A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BCD0E9C4-62AB-45B2-8362-A7AD1E4C03A7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BCD0E9C4-62AB-45B2-8362-A7AD1E4C03A7}.Debug|x64.ActiveCfg = Debug|Any CPU - {BCD0E9C4-62AB-45B2-8362-A7AD1E4C03A7}.Debug|x64.Build.0 = Debug|Any CPU - {BCD0E9C4-62AB-45B2-8362-A7AD1E4C03A7}.Debug|x86.ActiveCfg = Debug|Any CPU - {BCD0E9C4-62AB-45B2-8362-A7AD1E4C03A7}.Debug|x86.Build.0 = Debug|Any CPU - {BCD0E9C4-62AB-45B2-8362-A7AD1E4C03A7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BCD0E9C4-62AB-45B2-8362-A7AD1E4C03A7}.Release|Any CPU.Build.0 = Release|Any CPU - {BCD0E9C4-62AB-45B2-8362-A7AD1E4C03A7}.Release|x64.ActiveCfg = Release|Any CPU - {BCD0E9C4-62AB-45B2-8362-A7AD1E4C03A7}.Release|x64.Build.0 = Release|Any CPU - {BCD0E9C4-62AB-45B2-8362-A7AD1E4C03A7}.Release|x86.ActiveCfg = Release|Any CPU - {BCD0E9C4-62AB-45B2-8362-A7AD1E4C03A7}.Release|x86.Build.0 = Release|Any CPU - {61E7381C-C021-4048-A6F3-542E190F0D48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {61E7381C-C021-4048-A6F3-542E190F0D48}.Debug|Any CPU.Build.0 = Debug|Any CPU - {61E7381C-C021-4048-A6F3-542E190F0D48}.Debug|x64.ActiveCfg = Debug|Any CPU - {61E7381C-C021-4048-A6F3-542E190F0D48}.Debug|x64.Build.0 = Debug|Any CPU - {61E7381C-C021-4048-A6F3-542E190F0D48}.Debug|x86.ActiveCfg = Debug|Any CPU - {61E7381C-C021-4048-A6F3-542E190F0D48}.Debug|x86.Build.0 = Debug|Any CPU - {61E7381C-C021-4048-A6F3-542E190F0D48}.Release|Any CPU.ActiveCfg = Release|Any CPU - {61E7381C-C021-4048-A6F3-542E190F0D48}.Release|Any CPU.Build.0 = Release|Any CPU - {61E7381C-C021-4048-A6F3-542E190F0D48}.Release|x64.ActiveCfg = Release|Any CPU - {61E7381C-C021-4048-A6F3-542E190F0D48}.Release|x64.Build.0 = Release|Any CPU - {61E7381C-C021-4048-A6F3-542E190F0D48}.Release|x86.ActiveCfg = Release|Any CPU - {61E7381C-C021-4048-A6F3-542E190F0D48}.Release|x86.Build.0 = Release|Any CPU - {A878E197-31F3-4DBB-B8CC-6FBB4A53733E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A878E197-31F3-4DBB-B8CC-6FBB4A53733E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A878E197-31F3-4DBB-B8CC-6FBB4A53733E}.Debug|x64.ActiveCfg = Debug|Any CPU - {A878E197-31F3-4DBB-B8CC-6FBB4A53733E}.Debug|x64.Build.0 = Debug|Any CPU - {A878E197-31F3-4DBB-B8CC-6FBB4A53733E}.Debug|x86.ActiveCfg = Debug|Any CPU - {A878E197-31F3-4DBB-B8CC-6FBB4A53733E}.Debug|x86.Build.0 = Debug|Any CPU - {A878E197-31F3-4DBB-B8CC-6FBB4A53733E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A878E197-31F3-4DBB-B8CC-6FBB4A53733E}.Release|Any CPU.Build.0 = Release|Any CPU - {A878E197-31F3-4DBB-B8CC-6FBB4A53733E}.Release|x64.ActiveCfg = Release|Any CPU - {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 @@ -170,15 +90,6 @@ Global GlobalSection(NestedProjects) = preSolution {A16935F3-2E48-4D38-B08C-36E5ADE3B199} = {9E892FBB-74FD-464B-8939-6E4D9D70D99D} {2AF14B8E-56FF-4E54-99DA-C530D573814D} = {152E856C-48EA-42A1-A5F4-960819BEC170} - {E3500770-59F9-4526-9FFA-73E725C0008F} = {34A76D42-035A-4CD1-85B2-EC01D9CE3571} - {8AC0E934-6177-4D6D-9520-6E0931E527B9} = {34A76D42-035A-4CD1-85B2-EC01D9CE3571} - {3EE9B6D3-7ED6-43EE-9237-39E069042C65} = {34A76D42-035A-4CD1-85B2-EC01D9CE3571} - {53C796EB-20D2-4AF9-AAB9-130150005E21} = {34A76D42-035A-4CD1-85B2-EC01D9CE3571} - {FE396475-56EA-48AC-87B8-97EF6A66612F} = {53C796EB-20D2-4AF9-AAB9-130150005E21} - {3AAA2ECF-350F-4574-925F-21A909F41F42} = {3EE9B6D3-7ED6-43EE-9237-39E069042C65} - {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 diff --git a/README.md b/README.md index cedc4407..038b4bd8 100644 --- a/README.md +++ b/README.md @@ -162,21 +162,18 @@ For more information please check the official [Giraffe documentation](https://g ### Demo apps -There is a few sample applications which can be found in the [`/samples`](https://github.com/giraffe-fsharp/Giraffe/tree/master/samples) folder: - -| Sample | Description | -| ------ | ----------- | -| [GoogleAuthApp](https://github.com/giraffe-fsharp/Giraffe/tree/master/samples/GoogleAuthApp) | Demonstrates how Google Auth can be used with Giraffe. | -| [IdentityApp](https://github.com/giraffe-fsharp/Giraffe/tree/master/samples/IdentityApp) | Demonstrates how ASP.NET Core Identity can be used with Giraffe. | -| [JwtApp](https://github.com/giraffe-fsharp/Giraffe/tree/master/samples/JwtApp) | Demonstrates how JWT tokens can be used with Giraffe. | -| [SampleApp](https://github.com/giraffe-fsharp/Giraffe/tree/master/samples/SampleApp) | Generic sample application showcasing multiple features such as file uploads, cookie auth, model binding and validation, etc. | +There is a few sample applications which can be found in the [`samples`](https://github.com/giraffe-fsharp/samples) GitHub repository. Please check the `README.md` there for further information. ### Live apps -#### Buildstats.info +#### buildstats.info The web service [https://buildstats.info](https://buildstats.info) uses Giraffe to build rich SVG widgets for Git repositories. The application runs as a Docker container in the Google Container Engine (see [CI-BuiltStats on GitHub](https://github.com/dustinmoris/CI-BuildStats) for more information). +#### dusted.codes + +My personal blog [https://dusted.codes](https://dusted.codes) is also built with Giraffe and ASP.NET Core and all of the [source code is published on GitHub](https://github.com/dustinmoris/DustedCodes) for further reference. + More sample applications will be added in the future. ## Benchmarks @@ -197,53 +194,42 @@ Giraffe is built with the latest [.NET Core SDK](https://www.microsoft.com/net/d You can either install [Visual Studio 2017](https://www.visualstudio.com/vs/) which comes with the latest SDK or manually download and install the [.NET SDK 2.1](https://www.microsoft.com/net/download/core). -After installation you should be able to run the `.\build.ps1` script to successfully build, test and package the library. +After installation you should be able to run the `./build.ps1` script to successfully build, test and package the library. The build script supports the following flags: -- `-IncludeTests` will build and run the tests project as well -- `-IncludeSamples` will build and test the samples project as well -- `-All` will build and test all projects - `-Release` will build Giraffe with the `Release` configuration -- `-Pack` will create a NuGet package for Giraffe and giraffe-template. +- `-ExcludeTests` will skip the test project. This is useful to quickly validate any build errors. +- `-Pack` will create a NuGet package for Giraffe. +- `-ClearOnly` will delete all old build artifacts and not build anything itself. Examples: Only build the Giraffe project in `Debug` mode: ``` -PS > .\build.ps1 +PS > ./build.ps1 ``` Build the Giraffe project in `Release` mode: ``` -PS > .\build.ps1 -Release -``` - -Build the Giraffe project in `Debug` mode and also build and run the tests project: -``` -PS > .\build.ps1 -IncludeTests -``` - -Same as before, but also build and test the samples project: -``` -PS > .\build.ps1 -IncludeTests -IncludeSamples +PS > ./build.ps1 -Release ``` -One switch to build and test all projects: +Build the Giraffe project in `Debug` mode and skip running the tests project: ``` -PS > .\build.ps1 -All +PS > ./build.ps1 -ExcludeTests ``` Build and test all projects, use the `Release` build configuration and create all NuGet packages: ``` -PS > .\build.ps1 -Release -All -Pack +PS > ./build.ps1 -Release -Pack ``` ### Building on Linux or macOS In order to successfully run the build script on Linux or macOS you will have to [install PowerShell for Linux or Mac](https://github.com/PowerShell/PowerShell#get-powershell). -Additionally you will have to [install the latest version of Mono](http://www.mono-project.com/download/) and execute the `./build.sh` script which will set the correct `FrameworkPathOverride` before subsequently executing the `./build.ps1` PowerShell script. +Additionally you will also have to [install the latest version of Mono](http://www.mono-project.com/download/) in order to target full framework monikers during the build steps. ### Development environment diff --git a/build.ps1 b/build.ps1 index bc14aded..59728c38 100644 --- a/build.ps1 +++ b/build.ps1 @@ -6,9 +6,7 @@ param ( [switch] $Release, [switch] $ExcludeTests, - [switch] $ExcludeSamples, [switch] $Pack, - [switch] $Run, [switch] $ClearOnly ) @@ -30,10 +28,6 @@ if ($ClearOnly.IsPresent) $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" $version = Get-ProjectVersion $giraffe Update-AppVeyorBuildVersion $version @@ -61,25 +55,6 @@ if (!$ExcludeTests.IsPresent -and !$Run.IsPresent) dotnet-test $giraffeTests } -if (!$ExcludeSamples.IsPresent -and !$Run.IsPresent) -{ - Write-Host "Building and testing samples..." -ForegroundColor Magenta - - dotnet-build $identityApp - dotnet-build $jwtApp - dotnet-build $sampleApp - - dotnet-build $sampleAppTests - dotnet-test $sampleAppTests -} - -if ($Run.IsPresent) -{ - Write-Host "Launching sample application..." -ForegroundColor Magenta - dotnet-build $sampleApp - dotnet-run $sampleApp -} - if ($Pack.IsPresent) { Write-Host "Packaging Giraffe NuGet package..." -ForegroundColor Magenta diff --git a/jmeter-load-test.jmx b/jmeter-load-test.jmx deleted file mode 100644 index 97493077..00000000 --- a/jmeter-load-test.jmx +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - false - false - - - - - - - - continue - 100 - 0 - 10 - 0 - 30 - 5 - 1 - 60 - 5 - - false - -1 - - - - - - - - localhost - 5000 - - - /razor - GET - true - false - true - false - - - - - - - - Hello, Razor - - Assertion.response_data - false - 2 - - - - - false - - saveConfig - - - true - true - true - - true - false - true - false - false - false - false - false - false - false - true - false - false - false - false - 0 - true - true - true - true - true - - - - - - - - - true - - - - \ No newline at end of file diff --git a/samples/GoogleAuthApp/GoogleAuthApp/GoogleAuthApp.fsproj b/samples/GoogleAuthApp/GoogleAuthApp/GoogleAuthApp.fsproj deleted file mode 100644 index 6baaea1b..00000000 --- a/samples/GoogleAuthApp/GoogleAuthApp/GoogleAuthApp.fsproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - netcoreapp3.1 - portable - false - $(MSBuildThisFileDirectory) - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/samples/GoogleAuthApp/GoogleAuthApp/HttpsConfig.fs b/samples/GoogleAuthApp/GoogleAuthApp/HttpsConfig.fs deleted file mode 100644 index 5bb16e23..00000000 --- a/samples/GoogleAuthApp/GoogleAuthApp/HttpsConfig.fs +++ /dev/null @@ -1,94 +0,0 @@ -module GoogleAuthApp.HttpsConfig - -open System -open System.Net -open System.Security.Cryptography.X509Certificates -open Microsoft.AspNetCore.Hosting -open Microsoft.AspNetCore.Server.Kestrel.Core -open Microsoft.Extensions.DependencyInjection -open Microsoft.Extensions.Hosting - -// Follow the following instructions to set up -// a self signed certificate for localhost: -// https://blogs.msdn.microsoft.com/webdev/2017/11/29/configuring-https-in-asp-net-core-across-different-platforms/ - -type EndpointScheme = - | Http - | Https - -type EndpointConfiguration = - { - Host : string - Port : int option - Scheme : EndpointScheme - FilePath : string option - Password : string option - StoreName : string option - StoreLocation : string option - } - static member Default = - { - Host = "localhost" - Port = Some 8080 - Scheme = Http - FilePath = None - Password = None - StoreName = None - StoreLocation = None - } - -let loadCertificateFromStore (storeName : string) - (location : string) - (cfg : EndpointConfiguration) - (env : IWebHostEnvironment) = - use store = new X509Store(storeName, Enum.Parse location) - store.Open OpenFlags.ReadOnly - let cert = - store.Certificates.Find( - X509FindType.FindBySubjectName, - cfg.Host, - not (env.IsDevelopment())) - match cert.Count with - | 0 -> raise(InvalidOperationException(sprintf "Certificate not found for %s." cfg.Host)) - | _ -> cert.[0] - -let loadCertificate (cfg : EndpointConfiguration) (env : IWebHostEnvironment) = - match cfg.StoreName, cfg.StoreLocation, cfg.FilePath, cfg.Password with - | Some n, Some l, _, _ -> loadCertificateFromStore n l cfg env - | _, _, Some f, Some p -> new X509Certificate2(f, p) - | _, _, Some f, None -> new X509Certificate2(f) - | _ -> raise (InvalidOperationException("No valid certificate configuration found for the current endpoint.")) - -type KestrelServerOptions with - member this.ConfigureEndpoints (endpoints : EndpointConfiguration list) = - let env = this.ApplicationServices.GetRequiredService() - endpoints - |> List.iter (fun endpoint -> - let port = - match endpoint.Port with - | Some p -> p - | None -> - match endpoint.Scheme.Equals "https" with - | true -> 443 - | false -> 80 - - let ipAddresses = - match endpoint.Host.Equals "localhost" with - | true -> [ IPAddress.IPv6Loopback; IPAddress.Loopback ] - | false -> - match IPAddress.TryParse endpoint.Host with - | true, ip -> [ ip ] - | false, _ -> [ IPAddress.IPv6Any ] - - ipAddresses - |> List.iter (fun ip -> - this.Listen(ip, port, fun options -> - match endpoint.Scheme with - | Https -> - loadCertificate endpoint env - |> options.UseHttps - |> ignore - | Http -> () - ) - ) - ) \ No newline at end of file diff --git a/samples/GoogleAuthApp/GoogleAuthApp/Program.fs b/samples/GoogleAuthApp/GoogleAuthApp/Program.fs deleted file mode 100644 index 6197a10e..00000000 --- a/samples/GoogleAuthApp/GoogleAuthApp/Program.fs +++ /dev/null @@ -1,192 +0,0 @@ -module GoogleAuthApp.App - -open System -open Microsoft.AspNetCore.Http -open Microsoft.AspNetCore.Builder -open Microsoft.AspNetCore.Hosting -open Microsoft.AspNetCore.Authentication -open Microsoft.Extensions.Hosting -open Microsoft.Extensions.Logging -open Microsoft.Extensions.DependencyInjection -open FSharp.Control.Tasks.V2.ContextInsensitive -open Giraffe -open Giraffe.GiraffeViewEngine -open GoogleAuthApp.HttpsConfig - -// --------------------------------- -// Web app -// --------------------------------- - -module AuthSchemes = - - let cookie = "Cookies" - let google = "Google" - -module Urls = - - let index = "/" - let login = "/login" - let googleAuth = "/google-auth" - let user = "/user" - let logout = "/logout" - let missing = "/missing" - -module Views = - - let master (content: XmlNode list) = - html [] [ - head [] [ - title [] [ str "Google Auth Sample App" ] - ] - body [] content - ] - - let index = - [ - h1 [] [ str "Google Auth Sample App" ] - p [] [ str "Welcome to the Google Auth Sample App!" ] - ul [] [ - li [] [ a [ _href Urls.login ] [ str "Login" ] ] - li [] [ a [ _href Urls.user ] [ str "User profile" ] ] - ] - ] |> master - - let login = - [ - h1 [] [ str "Login" ] - p [] [ str "Pick one of the options to log in:" ] - ul [] [ - li [] [ a [ _href Urls.googleAuth ] [ str "Google" ] ] - li [] [ a [ _href Urls.missing ] [ str "Facebook" ] ] - li [] [ a [ _href Urls.missing ] [ str "Twitter" ] ] - ] - p [] [ - a [ _href Urls.index ] [ str "Return to home." ] - ] - ] |> master - - let user (claims : (string * string) seq) = - [ - h1 [] [ str "User details" ] - h2 [] [ str "Claims:" ] - ul [] [ - yield! claims |> Seq.map ( - fun (key, value) -> - li [] [ sprintf "%s: %s" key value |> str ] ) - ] - p [] [ - a [ _href Urls.logout ] [ str "Logout" ] - ] - ] |> master - - let notFound = - [ - h1 [] [ str "Not Found" ] - p [] [ str "The requested resource does not exist." ] - p [] [ str "Facebook and Twitter auth handlers have not been configured yet." ] - ul [] [ - li [] [ a [ _href Urls.index ] [ str "Return to home." ] ] - ] - ] |> master - -module Handlers = - - let index : HttpHandler = Views.index |> htmlView - let login : HttpHandler = Views.login |> htmlView - - let user : HttpHandler = - fun (next : HttpFunc) (ctx : HttpContext) -> - (ctx.User.Claims - |> Seq.map (fun c -> (c.Type, c.Value)) - |> Views.user - |> htmlView) next ctx - - let logout : HttpHandler = - signOut AuthSchemes.cookie - >=> redirectTo false Urls.index - - let challenge (scheme : string) (redirectUri : string) : HttpHandler = - fun (next : HttpFunc) (ctx : HttpContext) -> - task { - do! ctx.ChallengeAsync( - scheme, - AuthenticationProperties(RedirectUri = redirectUri)) - return! next ctx - } - - let googleAuth = challenge AuthSchemes.google Urls.user - - let authenticate : HttpHandler = - requiresAuthentication login - - let notFound : HttpHandler = - setStatusCode 404 >=> - (Views.notFound |> htmlView) - - let webApp : HttpHandler = - choose [ - GET >=> - choose [ - route Urls.index >=> index - route Urls.login >=> login - route Urls.user >=> authenticate >=> user - route Urls.logout >=> logout - route Urls.googleAuth >=> googleAuth - ] - notFound ] - - let error (ex : Exception) (logger : ILogger) = - logger.LogError(EventId(), ex, "An unhandled exception has occurred while executing the request.") - clearResponse >=> setStatusCode 500 >=> text ex.Message - -// --------------------------------- -// Config and Main -// --------------------------------- - -let configureApp (app : IApplicationBuilder) = - app.UseGiraffeErrorHandler(Handlers.error) - .UseAuthentication() - .UseGiraffe Handlers.webApp - -let configureServices (services : IServiceCollection) = - // Enable Authentication providers - services.AddAuthentication(fun o -> o.DefaultScheme <- AuthSchemes.cookie) - .AddCookie( - AuthSchemes.cookie, fun o -> - o.LoginPath <- PathString Urls.login - o.LogoutPath <- PathString Urls.logout) - .AddGoogle( - AuthSchemes.google, fun o -> - o.ClientId <- "" - o.ClientSecret <- "") - |> ignore - - // Add Giraffe dependencies - services.AddGiraffe() |> ignore - -let configureLogging (builder : ILoggingBuilder) = - let filter (l : LogLevel) = l.Equals LogLevel.Error - builder.AddFilter(filter).AddConsole().AddDebug() |> ignore - -[] -let main _ = - let endpoints = - [ - EndpointConfiguration.Default - { EndpointConfiguration.Default with - Port = Some 44340 - Scheme = Https - FilePath = Some "" - Password = Some "" } ] - Host.CreateDefaultBuilder() - .ConfigureWebHostDefaults( - fun webHostBuilder -> - webHostBuilder - .UseKestrel(fun o -> o.ConfigureEndpoints endpoints) - .Configure(configureApp) - .ConfigureServices(configureServices) - .ConfigureLogging(configureLogging) - |> ignore) - .Build() - .Run() - 0 \ No newline at end of file diff --git a/samples/IdentityApp/IdentityApp/IdentityApp.fsproj b/samples/IdentityApp/IdentityApp/IdentityApp.fsproj deleted file mode 100644 index f54d0a23..00000000 --- a/samples/IdentityApp/IdentityApp/IdentityApp.fsproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - netcoreapp3.1 - portable - false - $(MSBuildThisFileDirectory) - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/samples/IdentityApp/IdentityApp/Program.fs b/samples/IdentityApp/IdentityApp/Program.fs deleted file mode 100644 index c2f20431..00000000 --- a/samples/IdentityApp/IdentityApp/Program.fs +++ /dev/null @@ -1,266 +0,0 @@ -module SampleApp.App - -open System -open System.IO -open System.Text -open Microsoft.AspNetCore.Builder -open Microsoft.AspNetCore.Cors.Infrastructure -open Microsoft.AspNetCore.Hosting -open Microsoft.AspNetCore.Http -open Microsoft.Extensions.Hosting -open Microsoft.Extensions.Logging -open Microsoft.Extensions.DependencyInjection -open Microsoft.AspNetCore.Identity -open Microsoft.AspNetCore.Identity.EntityFrameworkCore -open Microsoft.EntityFrameworkCore -open FSharp.Control.Tasks.V2.ContextInsensitive -open Giraffe -open Giraffe.GiraffeViewEngine - -// --------------------------------- -// View engine -// --------------------------------- - -let masterPage (pageTitle : string) (content : XmlNode list) = - html [] [ - head [] [ - title [] [ str pageTitle ] - style [] [ rawText "label { display: inline-block; width: 80px; }" ] - ] - body [] [ - h1 [] [ str pageTitle ] - main [] content - ] - ] - -let indexPage = - [ - p [] [ - a [ _href "/register" ] [ str "Register" ] - ] - p [] [ - a [ _href "/user" ] [ str "User page" ] - ] - ] |> masterPage "Home" - -let registerPage = - [ - form [ _action "/register"; _method "POST" ] [ - div [] [ - label [] [ str "Email:" ] - input [ _name "Email"; _type "text" ] - ] - div [] [ - label [] [ str "User name:" ] - input [ _name "UserName"; _type "text" ] - ] - div [] [ - label [] [ str "Password:" ] - input [ _name "Password"; _type "password" ] - ] - input [ _type "submit" ] - ] - ] |> masterPage "Register" - -let loginPage (loginFailed : bool) = - [ - if loginFailed then yield p [ _style "color: Red;" ] [ str "Login failed." ] - - yield form [ _action "/login"; _method "POST" ] [ - div [] [ - label [] [ str "User name:" ] - input [ _name "UserName"; _type "text" ] - ] - div [] [ - label [] [ str "Password:" ] - input [ _name "Password"; _type "password" ] - ] - input [ _type "submit" ] - ] - yield p [] [ - str "Don't have an account yet?" - a [ _href "/register" ] [ str "Go to registration" ] - ] - ] |> masterPage "Login" - -let userPage (user : IdentityUser) = - [ - p [] [ - sprintf "User name: %s, Email: %s" user.UserName user.Email - |> str - ] - ] |> masterPage "User details" - -// --------------------------------- -// Web app -// --------------------------------- - -[] -type RegisterModel = - { - UserName : string - Email : string - Password : string - } - -[] -type LoginModel = - { - UserName : string - Password : string - } - -let showErrors (errors : IdentityError seq) = - errors - |> Seq.fold (fun acc err -> - sprintf "Code: %s, Description: %s" err.Code err.Description - |> acc.AppendLine : StringBuilder) (StringBuilder("")) - |> (fun x -> x.ToString()) - |> text - -let registerHandler : HttpHandler = - fun (next : HttpFunc) (ctx : HttpContext) -> - task { - let! model = ctx.BindFormAsync() - let user = IdentityUser(UserName = model.UserName, Email = model.Email) - let userManager = ctx.GetService>() - let! result = userManager.CreateAsync(user, model.Password) - - match result.Succeeded with - | false -> return! showErrors result.Errors next ctx - | true -> - let signInManager = ctx.GetService>() - do! signInManager.SignInAsync(user, true) - return! redirectTo false "/user" next ctx - } - -let loginHandler : HttpHandler = - fun (next : HttpFunc) (ctx : HttpContext) -> - task { - let! model = ctx.BindFormAsync() - let signInManager = ctx.GetService>() - let! result = signInManager.PasswordSignInAsync(model.UserName, model.Password, true, false) - match result.Succeeded with - | true -> return! redirectTo false "/user" next ctx - | false -> return! htmlView (loginPage true) next ctx - } - -let userHandler : HttpHandler = - fun (next : HttpFunc) (ctx : HttpContext) -> - task { - let userManager = ctx.GetService>() - let! user = userManager.GetUserAsync ctx.User - return! (user |> userPage |> htmlView) next ctx - } - -let mustBeLoggedIn : HttpHandler = - requiresAuthentication (redirectTo false "/login") - -let logoutHandler : HttpHandler = - fun (next : HttpFunc) (ctx : HttpContext) -> - task { - let signInManager = ctx.GetService>() - do! signInManager.SignOutAsync() - return! (redirectTo false "/") next ctx - } - -let webApp = - choose [ - GET >=> - choose [ - route "/" >=> htmlView indexPage - route "/register" >=> htmlView registerPage - route "/login" >=> htmlView (loginPage false) - - route "/logout" >=> mustBeLoggedIn >=> logoutHandler - route "/user" >=> mustBeLoggedIn >=> userHandler - ] - POST >=> - choose [ - route "/register" >=> registerHandler - route "/login" >=> loginHandler - ] - setStatusCode 404 >=> text "Not Found" ] - -// --------------------------------- -// Error handler -// --------------------------------- - -let errorHandler (ex : Exception) (logger : ILogger) = - logger.LogError(EventId(), ex, "An unhandled exception has occurred while executing the request.") - clearResponse >=> setStatusCode 500 >=> text ex.Message - -// --------------------------------- -// Main -// --------------------------------- - -let configureCors (builder : CorsPolicyBuilder) = - builder.WithOrigins("http://localhost:8080").AllowAnyMethod().AllowAnyHeader() |> ignore - -let configureApp (app : IApplicationBuilder) = - app.UseCors(configureCors) - .UseGiraffeErrorHandler(errorHandler) - .UseAuthentication() - .UseGiraffe webApp - -let configureServices (services : IServiceCollection) = - // Configure InMemory Db for sample application - services.AddDbContext>( - fun options -> - options.UseInMemoryDatabase("NameOfDatabase") |> ignore - ) |> ignore - - // Register Identity Dependencies - services.AddIdentity( - fun options -> - // Password settings - options.Password.RequireDigit <- true - options.Password.RequiredLength <- 8 - options.Password.RequireNonAlphanumeric <- false - options.Password.RequireUppercase <- true - options.Password.RequireLowercase <- false - - // Lockout settings - options.Lockout.DefaultLockoutTimeSpan <- TimeSpan.FromMinutes 30.0 - options.Lockout.MaxFailedAccessAttempts <- 10 - - // User settings - options.User.RequireUniqueEmail <- true - ) - .AddEntityFrameworkStores>() - .AddDefaultTokenProviders() - |> ignore - - // Configure app cookie - services.ConfigureApplicationCookie( - fun options -> - options.ExpireTimeSpan <- TimeSpan.FromDays 150.0 - options.LoginPath <- PathString "/login" - options.LogoutPath <- PathString "/logout" - ) |> ignore - - // Enable CORS - services.AddCors() |> ignore - - // Configure Giraffe dependencies - services.AddGiraffe() |> ignore - -let configureLogging (builder : ILoggingBuilder) = - let filter (l : LogLevel) = l.Equals LogLevel.Error - builder.AddFilter(filter).AddConsole().AddDebug() |> ignore - -[] -let main _ = - Host.CreateDefaultBuilder() - .ConfigureWebHostDefaults( - fun webHostBuilder -> - webHostBuilder - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .Configure(configureApp) - .ConfigureServices(configureServices) - .ConfigureLogging(configureLogging) - |> ignore) - .Build() - .Run() - 0 diff --git a/samples/JwtApp/JwtApp/JwtApp.fsproj b/samples/JwtApp/JwtApp/JwtApp.fsproj deleted file mode 100644 index 59060b22..00000000 --- a/samples/JwtApp/JwtApp/JwtApp.fsproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - netcoreapp3.1 - false - $(MSBuildThisFileDirectory) - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/samples/JwtApp/JwtApp/Program.fs b/samples/JwtApp/JwtApp/Program.fs deleted file mode 100644 index 1aefd3ff..00000000 --- a/samples/JwtApp/JwtApp/Program.fs +++ /dev/null @@ -1,106 +0,0 @@ -module JwtApp.App - -open System -open System.IO -open System.Security.Claims -open Microsoft.AspNetCore.Authentication -open Microsoft.AspNetCore.Authentication.JwtBearer -open Microsoft.AspNetCore.Builder -open Microsoft.AspNetCore.Hosting -open Microsoft.AspNetCore.Http -open Microsoft.Extensions.Logging -open Microsoft.Extensions.Hosting -open Microsoft.Extensions.DependencyInjection -open Microsoft.IdentityModel.Tokens -open Giraffe - -// --------------------------------- -// Web app -// --------------------------------- - -type SimpleClaim = { Type: string; Value: string } - -let authorize = - requiresAuthentication (challenge JwtBearerDefaults.AuthenticationScheme) - -let greet = - fun (next : HttpFunc) (ctx : HttpContext) -> - let claim = ctx.User.FindFirst "name" - let name = claim.Value - text ("Hello " + name) next ctx - -let showClaims = - fun (next : HttpFunc) (ctx : HttpContext) -> - let claims = ctx.User.Claims - let simpleClaims = Seq.map (fun (i : Claim) -> {Type = i.Type; Value = i.Value}) claims - json simpleClaims next ctx - -let webApp = - choose [ - GET >=> - choose [ - route "/" >=> text "Public endpoint." - route "/greet" >=> authorize >=> greet - route "/claims" >=> authorize >=> showClaims - ] - setStatusCode 404 >=> text "Not Found" ] - -// --------------------------------- -// Error handler -// --------------------------------- - -let errorHandler (ex : Exception) (logger : ILogger) = - logger.LogError(EventId(), ex, "An unhandled exception has occurred while executing the request.") - clearResponse >=> setStatusCode 500 >=> text ex.Message - -// --------------------------------- -// Config and Main -// --------------------------------- - -let configureApp (app : IApplicationBuilder) = - app.UseAuthentication() - .UseGiraffeErrorHandler(errorHandler) - .UseStaticFiles() - .UseGiraffe webApp - -let authenticationOptions (o : AuthenticationOptions) = - o.DefaultAuthenticateScheme <- JwtBearerDefaults.AuthenticationScheme - o.DefaultChallengeScheme <- JwtBearerDefaults.AuthenticationScheme - -let jwtBearerOptions (cfg : JwtBearerOptions) = - cfg.SaveToken <- true - cfg.IncludeErrorDetails <- true - cfg.Authority <- "https://accounts.google.com" - cfg.Audience <- "your-oauth-2.0-client-id.apps.googleusercontent.com" - cfg.TokenValidationParameters <- TokenValidationParameters ( - ValidIssuer = "accounts.google.com" - ) - -let configureServices (services : IServiceCollection) = - services - .AddGiraffe() - .AddAuthentication(authenticationOptions) - .AddJwtBearer(Action jwtBearerOptions) |> ignore - -let configureLogging (builder : ILoggingBuilder) = - let filter (l : LogLevel) = l.Equals LogLevel.Error - builder.AddFilter(filter).AddConsole().AddDebug() |> ignore - -[] -let main _ = - let contentRoot = Directory.GetCurrentDirectory() - let webRoot = Path.Combine(contentRoot, "WebRoot") - Host.CreateDefaultBuilder() - .ConfigureWebHostDefaults( - fun webHostBuilder -> - webHostBuilder - .UseKestrel() - .UseContentRoot(contentRoot) - .UseWebRoot(webRoot) - .Configure(configureApp) - .ConfigureServices(configureServices) - .ConfigureLogging(configureLogging) - |> ignore) - .Build() - .Run() - 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 deleted file mode 100644 index 912d180f..00000000 --- a/samples/SampleApp/SampleApp.Tests/SampleApp.Tests.fsproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - netcoreapp3.1 - SampleApp.Tests - portable - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/samples/SampleApp/SampleApp.Tests/Tests.fs b/samples/SampleApp/SampleApp.Tests/Tests.fs deleted file mode 100644 index d4867e96..00000000 --- a/samples/SampleApp/SampleApp.Tests/Tests.fs +++ /dev/null @@ -1,132 +0,0 @@ -module SampleApp.Tests - -open System -open System.Net -open System.Net.Http -open System.IO -open Microsoft.AspNetCore.Builder -open Microsoft.AspNetCore.Hosting -open Microsoft.AspNetCore.TestHost -open Microsoft.Extensions.DependencyInjection -open FSharp.Control.Tasks.V2.ContextInsensitive -open Xunit - -// --------------------------------- -// Test server/client setup -// --------------------------------- - -let createHost() = - WebHostBuilder() - .UseContentRoot(Directory.GetCurrentDirectory()) - .Configure(Action SampleApp.App.configureApp) - .ConfigureServices(Action SampleApp.App.configureServices) - -// --------------------------------- -// Helper functions -// --------------------------------- - -let get (client : HttpClient) (path : string) = - path - |> client.GetAsync - -let createRequest (method : HttpMethod) (path : string) = - let url = "http://127.0.0.1" + path - new HttpRequestMessage(method, url) - -let addCookiesFromResponse (response : HttpResponseMessage) - (request : HttpRequestMessage) = - request.Headers.Add("Cookie", response.Headers.GetValues("Set-Cookie")) - request - -let makeRequest (client : HttpClient) request = - request - |> client.SendAsync - -let isStatus (code : HttpStatusCode) (response : HttpResponseMessage) = - Assert.Equal(code, response.StatusCode) - response - -let isOfType (contentType : string) (response : HttpResponseMessage) = - Assert.Equal(contentType, response.Content.Headers.ContentType.MediaType) - response - -let readText (response : HttpResponseMessage) = - response.Content.ReadAsStringAsync() - -let shouldEqual expected actual = - Assert.Equal(expected, actual) - -// --------------------------------- -// Tests -// --------------------------------- - -[] -let ``Test / is routed to index`` () = - task { - use server = new TestServer(createHost()) - use client = server.CreateClient() - let! response = get client "/" - let! content = - response - |> isStatus HttpStatusCode.OK - |> readText - content - |> shouldEqual "index" - } - -[] -let ``Test /error returns status code 500`` () = - task { - use server = new TestServer(createHost()) - use client = server.CreateClient() - let! response = get client "/error" - let! content = - response - |> isStatus HttpStatusCode.InternalServerError - |> readText - content - |> shouldEqual "Something went wrong!" - } - -[] -let ``Test /user returns error when not logged in`` () = - task { - use server = new TestServer(createHost()) - use client = server.CreateClient() - let! response = get client "/user" - let! content = - response - |> isStatus HttpStatusCode.Unauthorized - |> readText - content - |> shouldEqual "Access Denied" - } - -[] -let ``Test /user/{id} returns success when logged in as user`` () = - task { - use server = new TestServer(createHost()) - use client = server.CreateClient() - let! response = get client "/login" - let! content = - response - |> isStatus HttpStatusCode.OK - |> readText - content - |> shouldEqual "Successfully logged in" - - // https://github.com/aspnet/Hosting/issues/191 - // The session cookie is not automatically forwarded to the next request. - // To overcome this we have to manually do: - let! response' = - "/user/1" - |> createRequest HttpMethod.Get - |> addCookiesFromResponse response - |> makeRequest client - let! content' = - response' - |> isStatus HttpStatusCode.OK - |> readText - content' - |> shouldEqual "User ID: 1" - } \ No newline at end of file diff --git a/samples/SampleApp/SampleApp/HtmlViews.fs b/samples/SampleApp/SampleApp/HtmlViews.fs deleted file mode 100644 index e2f9fc36..00000000 --- a/samples/SampleApp/SampleApp/HtmlViews.fs +++ /dev/null @@ -1,24 +0,0 @@ -module SampleApp.HtmlViews - -open Giraffe.GiraffeViewEngine -open SampleApp.Models - -let layout (content: XmlNode list) = - html [] [ - head [] [ - title [] [ str "Giraffe" ] - ] - body [] content - ] - -let partial () = - p [] [ str "Some partial text." ] - -let personView (model : Person) = - [ - div [ _class "container" ] [ - h3 [_title "Some title attribute"] [ sprintf "Hello, %s" model.Name |> str ] - a [ _href "https://github.com/giraffe-fsharp/Giraffe" ] [ str "Github" ] - ] - div [] [ partial() ] - ] |> layout \ No newline at end of file diff --git a/samples/SampleApp/SampleApp/Models.fs b/samples/SampleApp/SampleApp/Models.fs deleted file mode 100644 index 223c07a1..00000000 --- a/samples/SampleApp/SampleApp/Models.fs +++ /dev/null @@ -1,7 +0,0 @@ -namespace SampleApp.Models - -[] -type Person = - { - Name : string - } \ No newline at end of file diff --git a/samples/SampleApp/SampleApp/Program.fs b/samples/SampleApp/SampleApp/Program.fs deleted file mode 100644 index 11185fbb..00000000 --- a/samples/SampleApp/SampleApp/Program.fs +++ /dev/null @@ -1,203 +0,0 @@ -module SampleApp.App - -open System -open System.Security.Claims -open System.Threading -open Microsoft.AspNetCore.Builder -open Microsoft.AspNetCore.Hosting -open Microsoft.AspNetCore.Http -open Microsoft.AspNetCore.Http.Features -open Microsoft.AspNetCore.Authentication -open Microsoft.Extensions.Hosting -open Microsoft.AspNetCore.Authentication.Cookies -open Microsoft.Extensions.Configuration -open Microsoft.Extensions.Logging -open Microsoft.Extensions.DependencyInjection -open FSharp.Control.Tasks.V2.ContextInsensitive -open Giraffe -open SampleApp.Models -open SampleApp.HtmlViews - -// --------------------------------- -// Error handler -// --------------------------------- - -let errorHandler (ex : Exception) (logger : ILogger) = - logger.LogError(EventId(), ex, "An unhandled exception has occurred while executing the request.") - clearResponse >=> setStatusCode 500 >=> text ex.Message - -// --------------------------------- -// Web app -// --------------------------------- - -let authScheme = CookieAuthenticationDefaults.AuthenticationScheme - -let accessDenied = setStatusCode 401 >=> text "Access Denied" - -let mustBeUser = requiresAuthentication accessDenied - -let mustBeAdmin = - requiresAuthentication accessDenied - >=> requiresRole "Admin" accessDenied - -let mustBeJohn = - requiresAuthentication accessDenied - >=> authorizeUser (fun u -> u.HasClaim (ClaimTypes.Name, "John")) accessDenied - -let loginHandler = - fun (next : HttpFunc) (ctx : HttpContext) -> - task { - let issuer = "http://localhost:5000" - let claims = - [ - Claim(ClaimTypes.Name, "John", ClaimValueTypes.String, issuer) - Claim(ClaimTypes.Surname, "Doe", ClaimValueTypes.String, issuer) - Claim(ClaimTypes.Role, "Admin", ClaimValueTypes.String, issuer) - ] - let identity = ClaimsIdentity(claims, authScheme) - let user = ClaimsPrincipal(identity) - - do! ctx.SignInAsync(authScheme, user) - - return! text "Successfully logged in" next ctx - } - -let userHandler = - fun (next : HttpFunc) (ctx : HttpContext) -> - text ctx.User.Identity.Name next ctx - -let showUserHandler id = - mustBeAdmin >=> - text (sprintf "User ID: %i" id) - -let configuredHandler = - fun (next : HttpFunc) (ctx : HttpContext) -> - let configuration = ctx.GetService() - text configuration.["HelloMessage"] next ctx - -let fileUploadHandler = - fun (next : HttpFunc) (ctx : HttpContext) -> - task { - return! - (match ctx.Request.HasFormContentType with - | false -> RequestErrors.BAD_REQUEST "Bad request" - | true -> - ctx.Request.Form.Files - |> Seq.fold (fun acc file -> sprintf "%s\n%s" acc file.FileName) "" - |> text) next ctx - } - -let fileUploadHandler2 = - fun (next : HttpFunc) (ctx : HttpContext) -> - task { - let formFeature = ctx.Features.Get() - let! form = formFeature.ReadFormAsync CancellationToken.None - return! - (form.Files - |> Seq.fold (fun acc file -> sprintf "%s\n%s" acc file.FileName) "" - |> 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() - -[] -type Car = - { - Name : string - Make : string - Wheels : int - Built : DateTime - } - interface IModelValidation with - member this.Validate() = - if this.Wheels > 1 && this.Wheels <= 6 then Ok this - else Error (RequestErrors.BAD_REQUEST "Wheels must be a value between 2 and 6.") - -let parsingErrorHandler err = RequestErrors.BAD_REQUEST err - -let webApp = - choose [ - GET >=> - choose [ - route "/" >=> text "index" - route "/ping" >=> text "pong" - route "/error" >=> (fun _ _ -> failwith "Something went wrong!") - route "/login" >=> loginHandler - route "/logout" >=> signOut authScheme >=> text "Successfully logged out." - route "/user" >=> mustBeUser >=> userHandler - route "/john-only" >=> mustBeJohn >=> userHandler - routef "/user/%i" showUserHandler - route "/person" >=> (personView { Name = "Html Node" } |> htmlView) - route "/once" >=> (time() |> text) - route "/everytime" >=> warbler (fun _ -> (time() |> text)) - route "/configured" >=> configuredHandler - route "/upload" >=> fileUploadHandler - route "/upload2" >=> fileUploadHandler2 - route "/cache/1" >=> cacheHandler1 - route "/cache/2" >=> cacheHandler2 - route "/cache/3" >=> cacheHandler3 - ] - route "/car" >=> bindModel None json - route "/car2" >=> tryBindQuery parsingErrorHandler None (validateModel xml) - RequestErrors.notFound (text "Not Found") ] - -// --------------------------------- -// Main -// --------------------------------- - -let cookieAuth (o : CookieAuthenticationOptions) = - do - o.Cookie.HttpOnly <- true - o.Cookie.SecurePolicy <- CookieSecurePolicy.SameAsRequest - o.SlidingExpiration <- true - o.ExpireTimeSpan <- TimeSpan.FromDays 7.0 - -let configureApp (app : IApplicationBuilder) = - app.UseGiraffeErrorHandler(errorHandler) - .UseStaticFiles() - .UseAuthentication() - .UseResponseCaching() - .UseGiraffe webApp - -let configureServices (services : IServiceCollection) = - services - .AddResponseCaching() - .AddGiraffe() - .AddAuthentication(authScheme) - .AddCookie(cookieAuth) |> ignore - services.AddDataProtection() |> ignore - -let configureLogging (loggerBuilder : ILoggingBuilder) = - loggerBuilder.AddFilter(fun lvl -> lvl.Equals LogLevel.Error) - .AddConsole() - .AddDebug() |> ignore - -[] -let main _ = - Host.CreateDefaultBuilder() - .ConfigureWebHostDefaults( - fun webHostBuilder -> - webHostBuilder - .Configure(configureApp) - .ConfigureServices(configureServices) - .ConfigureLogging(configureLogging) - |> ignore) - .Build() - .Run() - 0 \ No newline at end of file diff --git a/samples/SampleApp/SampleApp/SampleApp.fsproj b/samples/SampleApp/SampleApp/SampleApp.fsproj deleted file mode 100644 index 51761d5f..00000000 --- a/samples/SampleApp/SampleApp/SampleApp.fsproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - netcoreapp3.1 - false - $(MSBuildThisFileDirectory) - - - - - - - - - - - - - \ No newline at end of file From 79b394c51a7d972db292176bce261d237eb2e8d3 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Mon, 13 Apr 2020 19:26:51 +0100 Subject: [PATCH 03/32] Readme update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 038b4bd8..80c06321 100644 --- a/README.md +++ b/README.md @@ -347,6 +347,6 @@ If you have any further questions feel free to reach out to me via any of the me ## Support -If you've got value from any of the content which I have created, but pull requests are not your thing, then I would also very much appreciate your support by buying me a coffee. +If you've got value from any of the content which I have created, but pull requests are not your thing, then I would also very much appreciate your support by buying me a coffee. Thank you! Buy Me A Coffee From 4c1e553941e382907bdc279257d1b8549ebd79c8 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Mon, 13 Apr 2020 19:35:58 +0100 Subject: [PATCH 04/32] Removed official support for `net461` target framework. --- RELEASE_NOTES.md | 4 ++++ src/Giraffe/Giraffe.fsproj | 3 +-- tests/Giraffe.Tests/Giraffe.Tests.fsproj | 9 ++++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index b5ccbf54..b6725c2e 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,10 @@ Release Notes ============= +## 5.0.0-alpha-001 + +- Removed official support for `net461` target framework. + ## 4.1.0 - Removed redundant dependencies diff --git a/src/Giraffe/Giraffe.fsproj b/src/Giraffe/Giraffe.fsproj index ccad0cce..d368bfb5 100644 --- a/src/Giraffe/Giraffe.fsproj +++ b/src/Giraffe/Giraffe.fsproj @@ -9,7 +9,7 @@ en-GB - netcoreapp3.0;netstandard2.0;net461 + netcoreapp3.1;netcoreapp3.0;netstandard2.0 portable Library true @@ -61,7 +61,6 @@ - diff --git a/tests/Giraffe.Tests/Giraffe.Tests.fsproj b/tests/Giraffe.Tests/Giraffe.Tests.fsproj index c963c51b..d62068da 100644 --- a/tests/Giraffe.Tests/Giraffe.Tests.fsproj +++ b/tests/Giraffe.Tests/Giraffe.Tests.fsproj @@ -1,6 +1,6 @@ - netcoreapp3.1;netcoreapp2.1;net461 + netcoreapp3.1;netcoreapp2.1 Giraffe.Tests @@ -29,11 +29,11 @@ - + - + @@ -47,10 +47,9 @@ - - + From 3590f4742196845aa3683e72ad439b41abbd58d5 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Mon, 13 Apr 2020 19:48:45 +0100 Subject: [PATCH 05/32] Removed the GiraffeViewEngine from the main project. The code will be moved into a separate NuGet package and the associated repo is in Giraffe.ViewEngine now. --- DOCUMENTATION.md | 449 +------------ Giraffe.sln | 15 - src/Giraffe/Giraffe.fsproj | 1 - src/Giraffe/GiraffeViewEngine.fs | 588 ------------------ src/Giraffe/ResponseWriters.fs | 91 --- .../Giraffe.Benchmarks.fsproj | 23 - tests/Giraffe.Benchmarks/Program.fs | 73 --- tests/Giraffe.Tests/Giraffe.Tests.fsproj | 1 - tests/Giraffe.Tests/GiraffeViewEngineTests.fs | 54 -- .../HttpContextExtensionsTests.fs | 33 - tests/Giraffe.Tests/HttpHandlerTests.fs | 42 -- 11 files changed, 2 insertions(+), 1368 deletions(-) delete mode 100644 src/Giraffe/GiraffeViewEngine.fs delete mode 100644 tests/Giraffe.Benchmarks/Giraffe.Benchmarks.fsproj delete mode 100644 tests/Giraffe.Benchmarks/Program.fs delete mode 100644 tests/Giraffe.Tests/GiraffeViewEngineTests.fs diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 77facb84..89a2bbdf 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -38,13 +38,6 @@ An in depth functional reference to all of Giraffe's default features. - [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) - - [Naming Convention](#naming-convention) - - [View Engine Best Practices](#view-engine-best-practices) - - [Custom Elements and Attributes](#custom-elements-and-attributes) - - [Rendering Views](#rendering-views) - - [Common View Engine Features](#common-view-engine-features) - [Serialization](#serialization) - [JSON](#json) - [XML](#xml) @@ -2918,451 +2911,13 @@ ASP.NET Core has its own [Response Compression Middleware](https://docs.microsof ## 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. +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 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. -### HTML Elements and Attributes - -HTML elements and attributes are defined as F# objects: - -```fsharp -let indexView = - html [] [ - head [] [ - title [] [ str "Giraffe Sample" ] - ] - body [] [ - h1 [] [ str "I |> F#" ] - p [ _class "some-css-class"; _id "someId" ] [ - str "Hello World" - ] - ] - ] -``` - -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: - -```fsharp -let someHtml = div [] [] -``` - -All `ParentNode` elements accept these two parameters: - -```fsharp -let someHtml = - div [ _id "someId"; _class "css-class" ] [ - a [ _href "https://example.org" ] [ str "Some text..." ] - ] -``` - -Most HTML tags are `ParentNode` elements, however there is a few HTML tags which cannot hold any child elements, such as `
`, `
` or `` tags. These are represented as `VoidElement` objects and only accept the `XmlAttribute list` as single parameter: - -```fsharp -let someHtml = - div [] [ - br [] - hr [ _class "css-class-for-hr" ] - p [] [ str "bla blah" ] - ] -``` - -Attributes are further classified into two different cases. First and most commonly there are `KeyValue` attributes: - -```fsharp -a [ - _href "http://url.com" - _target "_blank" - _class "class1" ] [ str "Click here" ] -``` - -As the name suggests, they have a key, such as `class` and a value such as the name of a CSS class. - -The second category of attributes are `Boolean` flags. There are not many but some HTML attributes which do not require any value (e.g. `async` or `defer` in script tags). The presence of such an attribute means that the feature is turned on, otherwise it is turned off: - -```fsharp -script [ _src "some.js"; _async ] [] -``` - -There's also a wealth of [accessibility attributes](https://www.w3.org/TR/html-aria/) available under the `Giraffe.GiraffeViewEngine.Accessibility` module (needs to be explicitly opened). - -### Text Content - -Naturally the most frequent content in any HTML document is pure text: - -```html -
-

This is text content

-

This is even more text content!

-
-``` - -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` (or the short alias `str`) functions: - -```fsharp -let someHtml = - div [] [ - p [] [ rawText "
Hello World
" ] - p [] [ encodedText "
Hello World
" ] - ] -``` - -The `rawText` function will create an object of type `XmlNode` where the content will be rendered in its original form and the `encodedText`/`str` 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>`. - -Please be aware that the the usage of `rawText` is mainly designed for edge cases where someone would purposefully want to inject HTML (or JavaScript) code into a rendered view. If not used carefully this could potentially lead to serious security vulnerabilities and therefore should be used only when explicitly required. - -Most cases and particularly any user provided content should always be output via the `encodedText`/`str` function. - -### Javascript event handlers - -It is possible to add JavaScript event handlers to HTML elements using the Giraffe View Engine. These event handlers (all prefixed with names starting with `_on`, for example `_onclick`, `_onmouseover`) can either execute inline JavaScript code or can invoke functions that are part of the `window` scope. - -This example illustrates how inline JavaScript could be used to log to the console when a button is clicked: - -```fsharp -let inlineJSButton = - button [_id "inline-js" - _onclick "console.log(\"Hello from the 'inline-js' button!\");"] [str "Say Hello" ] -``` - -There are some caveats with this approach, namely that -* it is not very scalable to write JavaScript inline in this manner, and more pressing -* the Giraffe View Engine HTML-encodes the text provided to the `_onX` attributes. - -To get around this, you can write dedicated scripts in your HTML and reference the functions from your event handlers: - -```fsharp -let page = - div [] [ - script [_type "application/javascript"] [ - rawText """ - window.greet = function () { - console.log("ping from the greet method"); - } - """ - ] - button [_id "script-tag-js" - _onclick "greet();"] [str "Say Hello"] - ] -``` - -Here it's important to note that we've included the text of our script using the `rawText` tag. This ensures that our text is not encoded by Giraffe so that it remains as we have written it. - -However, writing large quantities of JavaScript in this manner can be difficult, because you don't have access to the large ecosystem of javascript editor tooling. In this case you should write your functions in another script and use a `script` tag element to reference your script, then add the desired function to your HTML element's event handler. - -Say you had a JavaScript file named `greet.js` and had configured Giraffe to serve that script from the WebRoot. Let us also say that the content of that script was: - -```javascript -function greet() { - console.log("Hello from the greet function of greet.js!"); -} -``` - -Then, you could reference that javascript via a script element, and use `greet` in your event handler like so: - -```fsharp -let page = - html [] [ - head [] [ - script [_type "application/javascript" - _src "/greet.js"] [] // include our `greet.js` function dynamically - ] - body [] [ - button [_id "greet-btn" - _onclick "greet()"] [] // use the `greet()` function from `greet.js` to say hello - ] - ] -``` - -In this way, you can write `greet.js` with all of your expected tooling, and still hook up the event handlers all in one place in Giraffe. - -### Naming Convention - -The Giraffe View Engine has a naming convention which lets you easily determine the correct function name without having to know anything about the view engine's implementation. - -All HTML tags are defined as `XmlNode` elements under the exact same name as they are named in HTML. For example the `` tag would be `html [] []`, an `` tag would be `a [] []` and a `` or `` would be the `span [] []` or `canvas [] []` function. - -HTML attributes follow the same naming convention except that attributes have an underscore prepended. For example the `class` attribute would be `_class` and the `src` attribute would be `_src` in Giraffe. - -The underscore does not only help to distinguish an attribute from an element, but also avoid a naming conflict between tags and attributes of the same name (e.g. `
` vs. ``). - -If a HTML attribute has a hyphen in the name (e.g. `accept-charset`) then the equivalent Giraffe attribute would be written in camel case notion (e.g. `acceptCharset`). - -*Should you find a HTML tag or attribute missing in the Giraffe View Engine then you can either [create it yourself](#custom-elements-and-attributes) or send a [pull request on GitHub](https://github.com/giraffe-fsharp/Giraffe/pulls).* - -### View Engine Best Practices - -Due to the huge amount of available HTML tags and their fairly generic (and short) names (e.g. ``, `