├── tests
└── WebFrame.Tests
│ ├── Sample.txt
│ ├── About.html
│ ├── _Inner.liquid
│ ├── Index.liquid
│ ├── Program.fs
│ ├── paket.references
│ ├── Helpers.fs
│ ├── WebFrame.Tests.fsproj
│ └── BasicTests.fs
├── samples
├── AdvancedServer
│ ├── Text.txt
│ ├── Test.html
│ ├── appsettings.Development.json
│ ├── appsettings.json
│ ├── AdvancedServer.fsproj
│ ├── Properties
│ │ └── launchSettings.json
│ └── Program.fs
├── StandardServer
│ ├── Pages
│ │ ├── About.html
│ │ └── Index.html
│ ├── Properties
│ │ └── launchSettings.json
│ ├── StandardServer.fsproj
│ └── Program.fs
├── Minimal
│ ├── Program.fs
│ ├── Properties
│ │ └── launchSettings.json
│ └── Minimal.fsproj
├── Modules
│ ├── Program.fs
│ ├── Properties
│ │ └── launchSettings.json
│ └── Modules.fsproj
├── LocalServer
│ ├── Properties
│ │ └── launchSettings.json
│ ├── LocalServer.fsproj
│ └── Program.fs
└── TestServer
│ ├── Properties
│ └── launchSettings.json
│ ├── TestServer.fsproj
│ └── Program.fs
├── src
└── WebFrame
│ ├── paket.references
│ ├── Extensions.fs
│ ├── paket.template
│ ├── Logging.fs
│ ├── ServicesParts.fs
│ ├── AuthParts.fs
│ ├── Converters.fs
│ ├── NUGET_README.md
│ ├── CookieParts.fs
│ ├── HeaderParts.fs
│ ├── GlobalizationParts.fs
│ ├── Endpoints.fs
│ ├── ConfigParts.fs
│ ├── WebFrame.fsproj
│ ├── Http.fs
│ ├── RouteParts.fs
│ ├── QueryParts.fs
│ ├── Templating.fs
│ ├── Configuration.fs
│ ├── Exceptions.fs
│ ├── Services.fs
│ ├── RouteTypes.fs
│ ├── BodyParts.fs
│ ├── Library.fs
│ └── SystemConfig.fs
├── templates
├── All
│ └── Minimal
│ │ ├── .template.config
│ │ ├── ide.host.json
│ │ └── template.json
│ │ ├── Program.fs
│ │ └── Minimal.fsproj
├── README.md
└── templatepack.fsproj
├── .config
└── dotnet-tools.json
├── paket.dependencies
├── LICENSE
├── paket.lock
├── .github
└── workflows
│ ├── github-actions.yml
│ └── release.yml
├── .gitignore
├── WebFrame.sln
└── .paket
└── Paket.Restore.targets
/tests/WebFrame.Tests/Sample.txt:
--------------------------------------------------------------------------------
1 | sample file
--------------------------------------------------------------------------------
/tests/WebFrame.Tests/About.html:
--------------------------------------------------------------------------------
1 |
About
--------------------------------------------------------------------------------
/tests/WebFrame.Tests/_Inner.liquid:
--------------------------------------------------------------------------------
1 | Title
--------------------------------------------------------------------------------
/samples/AdvancedServer/Text.txt:
--------------------------------------------------------------------------------
1 | This is a sample text file.
--------------------------------------------------------------------------------
/tests/WebFrame.Tests/Index.liquid:
--------------------------------------------------------------------------------
1 | {% include "Inner" %}
2 | {{ hello }}
--------------------------------------------------------------------------------
/src/WebFrame/paket.references:
--------------------------------------------------------------------------------
1 | FSharp.Core
2 | Newtonsoft.Json
3 | DotLiquid
4 | Microsoft.AspNetCore.TestHost
--------------------------------------------------------------------------------
/tests/WebFrame.Tests/Program.fs:
--------------------------------------------------------------------------------
1 | namespace WebFrame.Tests
2 |
3 | module Program =
4 |
5 | []
6 | let main _ = 0
7 |
--------------------------------------------------------------------------------
/templates/All/Minimal/.template.config/ide.host.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/vs-2017.3.host",
3 | "symbolInfo": []
4 | }
--------------------------------------------------------------------------------
/tests/WebFrame.Tests/paket.references:
--------------------------------------------------------------------------------
1 | FSharp.Core
2 | Microsoft.NET.Test.Sdk
3 | Microsoft.AspNetCore.TestHost
4 | NUnit
5 | NUnit3TestAdapter
6 | FsUnit
--------------------------------------------------------------------------------
/templates/All/Minimal/Program.fs:
--------------------------------------------------------------------------------
1 | open WebFrame
2 |
3 | let app = App ()
4 |
5 | app.Get "/" <- fun serv -> serv.EndResponse "Hello World!"
6 |
7 | app.Run ()
8 |
--------------------------------------------------------------------------------
/samples/AdvancedServer/Test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Hello
6 |
7 |
8 | 123
9 |
10 |
--------------------------------------------------------------------------------
/.config/dotnet-tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "isRoot": true,
4 | "tools": {
5 | "paket": {
6 | "version": "7.0.2",
7 | "commands": [
8 | "paket"
9 | ]
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/samples/StandardServer/Pages/About.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | About
6 |
7 |
8 | About Page
9 |
10 |
--------------------------------------------------------------------------------
/samples/StandardServer/Pages/Index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Main
6 |
7 |
8 | Main Page
9 |
10 |
--------------------------------------------------------------------------------
/samples/AdvancedServer/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/samples/AdvancedServer/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*"
10 | }
11 |
--------------------------------------------------------------------------------
/src/WebFrame/Extensions.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.Extensions
2 |
3 | open System
4 |
5 | type Guid with
6 | static member FromString ( data: string ) =
7 | match Guid.TryParse data with
8 | | true, g -> Some g
9 | | _ -> None
10 |
--------------------------------------------------------------------------------
/samples/Minimal/Program.fs:
--------------------------------------------------------------------------------
1 | open WebFrame
2 |
3 | []
4 | let main _ =
5 | let app = App ()
6 |
7 | app.Get "/" <- fun serv -> serv.EndResponse "Hello World!"
8 |
9 | app.Run ()
10 |
11 | 0 // return an integer exit code
12 |
--------------------------------------------------------------------------------
/samples/Modules/Program.fs:
--------------------------------------------------------------------------------
1 | open WebFrame
2 |
3 | let api = AppModule "/api"
4 | api.Get "/" <- fun serv -> serv.EndResponse "Api"
5 |
6 | let app = App ()
7 | app.Get "/" <- fun serv -> serv.EndResponse "Main"
8 | app.Module "api" <- api
9 |
10 | app.Run ()
11 |
--------------------------------------------------------------------------------
/paket.dependencies:
--------------------------------------------------------------------------------
1 | source https://api.nuget.org/v3/index.json
2 |
3 | storage: none
4 | framework: net6.0
5 |
6 | nuget FSharp.Core >= 6.0
7 |
8 | nuget Newtonsoft.Json >= 13.0.1 < 14
9 |
10 | nuget DotLiquid >= 2.2.585 < 2.3
11 |
12 | nuget Microsoft.AspNetCore.TestHost ~> 6.0
13 |
14 | nuget FsUnit ~> 4.2
15 | nuget NUnit ~> 3.13
16 | nuget NUnit3TestAdapter ~> 4.2
17 |
18 | nuget Microsoft.NET.Test.Sdk ~> 17.1
--------------------------------------------------------------------------------
/src/WebFrame/paket.template:
--------------------------------------------------------------------------------
1 | type project
2 | id RussBaz.WebFrame
3 | version 0.1
4 | authors RussBaz
5 | licenseExpression MIT
6 | licenseUrl https://licenses.nuget.org/MIT
7 | readme ./NUGET_README.md
8 | description
9 | F# framework for rapid prototyping with ASP.NET Core
10 | projectUrl https://github.com/RussBaz/WebFrame
11 | repositoryUrl https://github.com/RussBaz/WebFrame
12 | files
13 | ./NUGET_README.md ==> .
--------------------------------------------------------------------------------
/templates/All/Minimal/Minimal.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/templates/README.md:
--------------------------------------------------------------------------------
1 | # WebFrame Templates
2 | Templates for starting your journey with the WebFrame
3 | * `webframe`: minimal - Single file template (+ a project file)
4 | # Quickstart
5 | 1. Get the templates: `dotnet new --install "RussBaz.WebFrame.Templates::*"`
6 | 2. Create a new project (e.g. minimal): `dotnet new webframe`
7 | 3. Start the server: `dotnet run`
8 | 4. Read the docs and check out the samples: https://github.com/RussBaz/WebFrame
--------------------------------------------------------------------------------
/samples/Minimal/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true
5 | },
6 | "profiles": {
7 | "Minimal": {
8 | "commandName": "Project",
9 | "launchBrowser": true,
10 | "applicationUrl": "http://localhost:1456",
11 | "environmentVariables": {
12 | "ASPNETCORE_ENVIRONMENT": "Development"
13 | }
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/samples/Modules/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true
5 | },
6 | "profiles": {
7 | "Modules": {
8 | "commandName": "Project",
9 | "launchBrowser": true,
10 | "applicationUrl": "http://localhost:55839",
11 | "environmentVariables": {
12 | "ASPNETCORE_ENVIRONMENT": "Development"
13 | }
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/samples/LocalServer/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true
5 | },
6 | "profiles": {
7 | "LocalServer": {
8 | "commandName": "Project",
9 | "launchBrowser": true,
10 | "applicationUrl": "http://localhost:15779",
11 | "environmentVariables": {
12 | "ASPNETCORE_ENVIRONMENT": "Development"
13 | }
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/samples/TestServer/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true
5 | },
6 | "profiles": {
7 | "TestServer": {
8 | "commandName": "Project",
9 | "launchBrowser": true,
10 | "applicationUrl": "http://localhost:61014",
11 | "environmentVariables": {
12 | "ASPNETCORE_ENVIRONMENT": "Development"
13 | }
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/samples/StandardServer/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true
5 | },
6 | "profiles": {
7 | "StandardServer": {
8 | "commandName": "Project",
9 | "launchBrowser": true,
10 | "applicationUrl": "http://localhost:20599",
11 | "environmentVariables": {
12 | "ASPNETCORE_ENVIRONMENT": "Development"
13 | }
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/samples/Minimal/Minimal.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/samples/Modules/Modules.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/samples/TestServer/TestServer.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/samples/LocalServer/LocalServer.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/samples/StandardServer/StandardServer.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/samples/TestServer/Program.fs:
--------------------------------------------------------------------------------
1 | open System.Threading.Tasks
2 |
3 | open Microsoft.AspNetCore.TestHost
4 |
5 | open WebFrame
6 |
7 | let app = App ()
8 |
9 | app.Get "/" <- fun serv -> serv.EndResponse "index"
10 |
11 | let t = [
12 | task {
13 | use! server = app.TestServer ()
14 | let client = server.GetTestClient ()
15 |
16 | let! r = client.GetAsync "/"
17 | let! c = r.Content.ReadAsStringAsync ()
18 |
19 | if c <> "index" then failwith "wrong content"
20 |
21 | return ()
22 | } :> Task
23 | ]
24 |
25 | t |> Array.ofList |> Task.WaitAll
26 |
--------------------------------------------------------------------------------
/src/WebFrame/Logging.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.Logging
2 |
3 | open Microsoft.Extensions.Logging
4 |
5 | type Logger ( logFactory: Lazy, name: string ) =
6 | let logger = lazy ( logFactory.Value.CreateLogger name )
7 |
8 | member _.Information ( message: string ) = logger.Value.LogInformation message
9 | member _.Warning ( message: string ) = logger.Value.LogWarning message
10 | member _.Error ( message: string ) = logger.Value.LogError message
11 | member _.Critical ( message: string ) = logger.Value.LogCritical message
12 | member _.Debug ( message: string ) = logger.Value.LogDebug message
13 | member _.Trace ( message: string ) = logger.Value.LogTrace message
14 |
--------------------------------------------------------------------------------
/samples/AdvancedServer/AdvancedServer.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 |
7 |
8 |
9 |
10 |
11 | true
12 | PreserveNewest
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/tests/WebFrame.Tests/Helpers.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.Tests.Helpers
2 |
3 | open System
4 |
5 | type EnvVarAction =
6 | | SetVarTo of string
7 | | DeleteVar
8 |
9 | type EnvVar ( name: string, value: string ) =
10 | let action =
11 | match Environment.GetEnvironmentVariable name with
12 | | null -> DeleteVar
13 | | v -> SetVarTo v
14 |
15 | let restore () =
16 | match action with
17 | | SetVarTo v -> Environment.SetEnvironmentVariable ( name, v )
18 | | DeleteVar -> Environment.SetEnvironmentVariable ( name, null )
19 |
20 | do Environment.SetEnvironmentVariable ( name, value )
21 | interface IDisposable with
22 | member _.Dispose() = restore ()
23 |
--------------------------------------------------------------------------------
/src/WebFrame/ServicesParts.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.ServicesParts
2 |
3 | open System
4 |
5 | open WebFrame.Exceptions
6 |
7 | type GenericServiceProvider ( provider: IServiceProvider ) =
8 | member _.Optional<'T> () =
9 | let t = typeof<'T>
10 | match provider.GetService t with
11 | | null -> None
12 | | v ->
13 | try
14 | v :?> 'T |> Some
15 | with
16 | | :? InvalidCastException -> None
17 |
18 | member this.Required<'T> () =
19 | let t = typeof<'T>
20 | match this.Optional<'T> () with
21 | | Some v -> v
22 | | None -> raise ( MissingRequiredDependencyException t.Name )
23 |
24 | member this.Get<'T> ( d: unit -> 'T ) = this.Optional<'T> () |> Option.defaultWith d
25 | member this.Raw = provider
26 |
--------------------------------------------------------------------------------
/samples/AdvancedServer/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:43179",
7 | "sslPort": 44324
8 | }
9 | },
10 | "profiles": {
11 | "IIS Express": {
12 | "commandName": "IISExpress",
13 | "launchBrowser": true,
14 | "environmentVariables": {
15 | "ASPNETCORE_ENVIRONMENT": "Development"
16 | }
17 | },
18 | "AdvancedServer": {
19 | "commandName": "Project",
20 | "dotnetRunMessages": "true",
21 | "launchBrowser": true,
22 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
23 | "environmentVariables": {
24 | "ASPNETCORE_ENVIRONMENT": "Development"
25 | }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/WebFrame/AuthParts.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.AuthParts
2 |
3 | open WebFrame.Exceptions
4 |
5 | type IAuthenticationProvider<'SP, 'T> =
6 | abstract member onAuthenticate: ( 'SP -> 'T option ) with get, set
7 |
8 | type AuthenticationProvider<'SP, 'T> () =
9 | interface IAuthenticationProvider<'SP, 'T> with
10 | member val onAuthenticate = raise ( MissingAuthenticationException () ) with get, set
11 |
12 | type UserManager<'SP, 'T> ( provider: Lazy>, serv: 'SP ) =
13 | member this.Required () = this.Optional () |> Option.defaultWith ( fun () -> raise ( NotAuthneticatedException () ) )
14 | member this.Optional () = provider.Value.onAuthenticate serv
15 |
16 | type Auth<'SP, 'T> ( authProvider: Lazy>, serv: 'SP ) =
17 | member val User = UserManager ( authProvider, serv )
18 |
--------------------------------------------------------------------------------
/tests/WebFrame.Tests/WebFrame.Tests.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | false
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/WebFrame/Converters.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.Converters
2 |
3 | open System
4 |
5 | open WebFrame.Extensions
6 |
7 | let canParse<'T> () =
8 | match typeof<'T> with
9 | | t when t.IsPrimitive -> true
10 | | t when t = typeof -> true
11 | | t when t = typeof -> true
12 | | t when t = typeof -> true
13 | | _ -> false
14 |
15 | let convertTo<'T> ( data: string ) =
16 | try
17 | let t = typeof<'T>
18 | if canParse<'T> () then
19 | match t with
20 | | t when t = typeof -> Guid.FromString data |> Option.map ( fun i -> i :> obj :?> 'T )
21 | | t -> Convert.ChangeType ( data, t ) :?> 'T |> Some
22 | else None
23 | with
24 | | :? InvalidCastException
25 | | :? FormatException
26 | | :? OverflowException
27 | | :? ArgumentNullException -> None
28 |
--------------------------------------------------------------------------------
/src/WebFrame/NUGET_README.md:
--------------------------------------------------------------------------------
1 | # WebFrame
2 | [](https://github.com/russbaz/webframe/actions/workflows/github-actions.yml)
3 | [](https://www.nuget.org/packages/RussBaz.WebFrame/)
4 | [](https://www.nuget.org/packages/RussBaz.WebFrame.Templates/)
5 |
6 | F# framework for rapid prototyping with ASP.NET Core
7 |
8 | ## Quickstart
9 | Documentation: https://github.com/RussBaz/WebFrame
10 |
11 | Installation:
12 |
13 | 1. Get the templates: `dotnet new --install "RussBaz.WebFrame.Templates::*"`
14 | 2. Create a new project (e.g. minimal): `dotnet new webframe`
15 | 3. Start the server: `dotnet run`
16 |
17 | ```F#
18 | open WebFrame
19 |
20 | let app = App ()
21 |
22 | app.Get "/" <- fun serv -> serv.EndResponse "Hello World!"
23 |
24 | app.Run ()
25 | ```
26 |
--------------------------------------------------------------------------------
/templates/All/Minimal/.template.config/template.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/template",
3 | "author": "RussBaz",
4 | "classifications": [
5 | "Web",
6 | "WebFrame",
7 | "Minimal"
8 | ],
9 | "identity": "WebFrameTemplate.MinimalProject",
10 | "name": "WebFrame Templates: Minimal Project",
11 | "shortName": "webframe",
12 | "tags": {
13 | "language": "F#",
14 | "type": "project"
15 | },
16 | "sourceName": "Minimal",
17 | "preferNameDirectory": true,
18 | "symbols": {
19 | "Framework": {
20 | "type": "parameter",
21 | "description": "The target framework for the project.",
22 | "datatype": "choice",
23 | "choices": [
24 | {
25 | "choice": "net6.0",
26 | "description": "Target .NET 6"
27 | }
28 | ],
29 | "defaultValue": "net6.0",
30 | "replaces": "net6.0"
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/src/WebFrame/CookieParts.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.CookieParts
2 |
3 | open Microsoft.AspNetCore.Http
4 |
5 | open WebFrame.Exceptions
6 |
7 | type Cookies ( req: HttpRequest, res: HttpResponse ) =
8 | let cIn = req.Cookies
9 | let cOut = res.Cookies
10 |
11 | member this.Required ( k: string ) = k |> this.Optional |> Option.defaultWith ( fun _ -> MissingRequiredCookieException k |> raise )
12 | member this.Get ( k: string ) ( d: string ) = k |> this.Optional |> Option.defaultValue d
13 | member this.Optional ( k: string ) =
14 | match cIn.TryGetValue k with
15 | | true, v -> Some v
16 | | _ -> None
17 |
18 | member this.Set ( k: string ) ( v: string ) = cOut.Append ( k, v )
19 | member this.SetWithOptions ( k: string ) ( v: string ) ( o: CookieOptions ) = cOut.Append ( k, v, o )
20 | member this.Delete ( k: string ) = cOut.Delete k
21 | member this.DeleteWithOptions ( k: string ) ( o: CookieOptions ) = cOut.Delete ( k, o )
22 |
23 | member val Raw = {| In = cIn; Out = cOut |}
24 |
--------------------------------------------------------------------------------
/templates/templatepack.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Template
5 | RussBaz.WebFrame.Templates
6 | WebFrame Templates
7 | RussBaz
8 | Templates for WebFrame framework
9 | README.md
10 | net6.0
11 | 3390;$(WarnOn)
12 | true
13 | false
14 | content
15 | $(NoWarn);NU5128
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/WebFrame/HeaderParts.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.HeaderParts
2 |
3 | open Microsoft.AspNetCore.Http
4 |
5 | open WebFrame.Exceptions
6 |
7 | type Headers ( req: HttpRequest, res: HttpResponse ) =
8 | let hIn = req.Headers
9 | let hOut = res.Headers
10 |
11 | member _.All ( k: string ) = k |> hIn.GetCommaSeparatedValues |> List.ofArray
12 |
13 | member this.Get ( k: string ) ( d: string ) = k |> this.Optional |> Option.defaultValue d
14 | member this.Optional ( k: string ) = k |> this.All |> List.tryHead
15 | member this.Required ( k: string ) = k |> this.Optional |> Option.defaultWith ( fun _ -> MissingRequiredHeaderException k |> raise )
16 |
17 | member this.Set ( k: string ) ( v: string list ) = hOut.SetCommaSeparatedValues ( k, Array.ofList v )
18 | member this.Append ( k: string ) ( v: string ) = hOut.AppendCommaSeparatedValues ( k, [| v |] )
19 | member this.Delete ( k: string ) = hOut.Remove k |> ignore
20 |
21 | member this.Count ( k: string ) = k |> this.All |> List.length
22 | member val Raw = {| In = hIn; Out = hOut |}
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 RussBaz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/samples/LocalServer/Program.fs:
--------------------------------------------------------------------------------
1 | open System
2 | open System.IO
3 |
4 | open WebFrame
5 |
6 | // The first argument is the current program name
7 | let argv = Environment.GetCommandLineArgs ()
8 |
9 | let app = App ()
10 |
11 | let location =
12 | argv
13 | |> Array.tail
14 | |> Array.tryHead
15 | |> Option.map Path.GetFullPath
16 | |> Option.defaultWith ( fun _ -> invalidArg "path" "Could not find the specified file path." )
17 |
18 | app.Log.Information $"Preparing to display contents of the following folder: {location}"
19 |
20 | app.Services.StaticFiles.Enabled <- true // Serving Static Files is disabled by default
21 | app.Services.StaticFiles.AllowBrowsing <- true // Only enable this if absolutely necessary
22 | app.Services.StaticFiles.WebRoot <- "." // Default location: wwwroot
23 |
24 | // The next line adds a prefix to all static files
25 | // and it must be a valid path
26 | // app.Services.StaticFiles.Route <- "/static"
27 |
28 | // The root location for serving any files
29 | app.Services.ContentRoot <- location
30 |
31 | app.Log.Information $"Displaying contents of the following folder: {location}"
32 |
33 | app.Run ()
34 |
--------------------------------------------------------------------------------
/samples/StandardServer/Program.fs:
--------------------------------------------------------------------------------
1 | open System
2 |
3 | open WebFrame
4 | open type WebFrame.Endpoints.Helpers
5 |
6 | []
7 | let main argv =
8 | let items = [ "todo1"; "todo2"; "todo3" ]
9 |
10 | let api = AppModule "/api"
11 |
12 | // Returning items
13 | api.Get "/" <- fun serv ->
14 | serv.EndResponse items
15 |
16 | // Adding items
17 | // By sending an item Name as a string field in a form
18 | api.Post "/" <- fun serv ->
19 | // If a required property in user input is not found,
20 | // then 400 error is issued automatically
21 | let itemName = serv.Body.Form.Required "name"
22 |
23 | if items |> List.contains itemName then
24 | serv.StatusCode <- 409
25 | printfn $"Item {itemName} already exists"
26 | else
27 | serv.StatusCode <- 201
28 | printfn $"Adding a new item {itemName}"
29 |
30 | serv.EndResponse ()
31 |
32 | let app = App argv
33 |
34 | app.Get "/" <- page "Pages/Index.html"
35 | app.Get "/About" <- page "Pages/About.html"
36 |
37 | app.Module "ToDoApi" <- api
38 |
39 | app.Run ()
40 |
41 | 0 // exit code
42 |
--------------------------------------------------------------------------------
/src/WebFrame/GlobalizationParts.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.GlobalizationParts
2 |
3 | open System
4 | open System.Globalization
5 | open Microsoft.AspNetCore.Http
6 | open WebFrame.Configuration
7 |
8 | type Globalization ( ctx: HttpContext, conf: Lazy ) =
9 | member _.RequestCulture with get () =
10 | let t = ctx.Request.GetTypedHeaders ()
11 | let l = t.AcceptLanguage
12 | let d = conf.Value.DefaultCulture
13 | let allowed = conf.Value.AllowedCultures |> List.map ( fun i -> i.Name ) |> set
14 |
15 | if l.Count < 1 then
16 | d
17 | else
18 | l
19 | |> Seq.map ( fun i -> i.Value.Value, i.Quality |> Option.ofNullable |> Option.defaultValue 1 )
20 | |> Seq.groupBy snd
21 | |> Seq.sortByDescending fst
22 | |> Seq.head
23 | |> snd
24 | |> Seq.head
25 | |> fun ( a, _ ) ->
26 | try
27 | if allowed |> Set.contains a then
28 | CultureInfo a
29 | else
30 | d
31 | with
32 | | :? ArgumentNullException -> d
33 | | :? CultureNotFoundException -> d
34 |
--------------------------------------------------------------------------------
/src/WebFrame/Endpoints.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.Endpoints
2 |
3 | open System.Threading.Tasks
4 | open WebFrame.Http
5 | open WebFrame.Services
6 |
7 | type Helpers () =
8 | static member always ( h: string ) = fun _ -> TextResponse h
9 | static member always ( h: HttpWorkload ) = fun _ -> h
10 | static member always ( h: unit->HttpWorkload ) = fun _ -> h ()
11 | static member alwaysTask ( h: string ) = fun _ -> task { return TextResponse h }
12 | static member alwaysTask ( h: HttpWorkload ) = fun _ -> task { return h }
13 | static member alwaysTask ( h: unit->Task ) = fun _ -> task { return! h () }
14 | static member page ( p: string ) =
15 | if not <| p.EndsWith ".html" then failwith $"The specified file '{p}' is not an HTML page."
16 | fun ( serv: RequestServices ) ->
17 | serv.Headers.Set "Content-Type" [ "text/html" ]
18 | FileResponse p
19 | static member file ( path: string ) = fun _ -> FileResponse path
20 | static member file ( path: string, contentType: string ) =
21 | fun ( serv: RequestServices ) ->
22 | serv.Headers.Set "Content-Type" [ contentType ]
23 | FileResponse path
24 |
25 | // Please ignore - left for the future development
26 | type private RouteBuilder () =
27 | member this.A = 9
28 | type EndpointConfig () =
29 | member this.A = 0
30 |
--------------------------------------------------------------------------------
/paket.lock:
--------------------------------------------------------------------------------
1 | STORAGE: NONE
2 | RESTRICTION: == net6.0
3 | NUGET
4 | remote: https://api.nuget.org/v3/index.json
5 | DotLiquid (2.2.595)
6 | FSharp.Core (6.0.3)
7 | FsUnit (4.2)
8 | FSharp.Core (>= 5.0.2)
9 | NETStandard.Library (>= 2.0.3)
10 | NUnit (>= 3.13.2 < 3.14)
11 | Microsoft.AspNetCore.TestHost (6.0.3)
12 | System.IO.Pipelines (>= 6.0.2)
13 | Microsoft.CodeCoverage (17.1)
14 | Microsoft.NET.Test.Sdk (17.1)
15 | Microsoft.CodeCoverage (>= 17.1)
16 | Microsoft.TestPlatform.TestHost (>= 17.1)
17 | Microsoft.NETCore.Platforms (6.0.2)
18 | Microsoft.TestPlatform.ObjectModel (17.1)
19 | NuGet.Frameworks (>= 5.11)
20 | System.Reflection.Metadata (>= 1.6)
21 | Microsoft.TestPlatform.TestHost (17.1)
22 | Microsoft.TestPlatform.ObjectModel (>= 17.1)
23 | Newtonsoft.Json (>= 9.0.1)
24 | NETStandard.Library (2.0.3)
25 | Microsoft.NETCore.Platforms (>= 1.1)
26 | Newtonsoft.Json (13.0.1)
27 | NuGet.Frameworks (6.1)
28 | NUnit (3.13.2)
29 | NETStandard.Library (>= 2.0)
30 | NUnit3TestAdapter (4.2.1)
31 | System.Collections.Immutable (6.0)
32 | System.Runtime.CompilerServices.Unsafe (>= 6.0)
33 | System.IO.Pipelines (6.0.2)
34 | System.Reflection.Metadata (6.0.1)
35 | System.Collections.Immutable (>= 6.0)
36 | System.Runtime.CompilerServices.Unsafe (6.0)
37 |
--------------------------------------------------------------------------------
/src/WebFrame/ConfigParts.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.ConfigParts
2 |
3 | open Microsoft.AspNetCore.Hosting
4 |
5 | open Microsoft.Extensions.Configuration
6 | open Microsoft.Extensions.Hosting
7 |
8 | open WebFrame.Exceptions
9 | open WebFrame.Converters
10 |
11 | type RuntimeConfigs ( conf: Lazy, env: Lazy ) =
12 | member _.String ( name: string ) =
13 | let r = conf.Value.[ name ]
14 |
15 | match box r with
16 | | null -> None
17 | | _ -> Some r
18 |
19 | member this.Get<'T> ( name: string ) ( d: 'T ) =
20 | name
21 | |> this.Optional
22 | |> Option.defaultValue d
23 |
24 | member this.Required<'T> ( name: string ) =
25 | name
26 | |> this.Optional<'T>
27 | |> Option.defaultWith ( fun _ -> MissingRequiredConfigException name |> raise )
28 |
29 | member this.Optional<'T> ( name: string ) =
30 | name
31 | |> this.String
32 | |> Option.bind convertTo<'T>
33 |
34 | member _.Raw = conf.Value
35 | member _.ApplicationName = env.Value.ApplicationName
36 | member _.EnvironmentName = env.Value.EnvironmentName
37 | member _.IsDevelopment = env.Value.IsDevelopment ()
38 | member _.IsStaging = env.Value.IsStaging ()
39 | member _.IsProduction = env.Value.IsProduction ()
40 | member _.IsEnvironment name = env.Value.IsEnvironment name
41 |
--------------------------------------------------------------------------------
/src/WebFrame/WebFrame.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | true
6 | 3390;$(WarnOn)
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/WebFrame/Http.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.Http
2 |
3 | open System
4 | open System.Threading.Tasks
5 |
6 | open Microsoft.AspNetCore.Http
7 |
8 | open WebFrame.Configuration
9 |
10 | type HttpMethod =
11 | | CONNECT
12 | | DELETE
13 | | GET
14 | | HEAD
15 | | OPTIONS
16 | | PATCH
17 | | POST
18 | | PUT
19 | | TRACE
20 | with
21 | override this.ToString () =
22 | match this with
23 | | CONNECT -> "CONNECT"
24 | | DELETE -> "DELETE"
25 | | GET-> "GET"
26 | | HEAD -> "HEAD"
27 | | OPTIONS -> "OPTIONS"
28 | | PATCH -> "PATCH"
29 | | POST -> "POST"
30 | | PUT-> "PUT"
31 | | TRACE -> "TRACE"
32 | static member Any = [ CONNECT; DELETE; GET; HEAD; OPTIONS; PATCH; POST; PUT; TRACE ]
33 | static member FromString ( s: string ) =
34 | match s with
35 | | "CONNECT" -> CONNECT
36 | | "DELETE" -> DELETE
37 | | "GET" -> GET
38 | | "HEAD" -> HEAD
39 | | "OPTIONS" -> OPTIONS
40 | | "PATCH" -> PATCH
41 | | "POST" -> POST
42 | | "PUT" -> PUT
43 | | "TRACE" -> TRACE
44 | | s -> failwith $"Unknown HTTP Method {s}"
45 |
46 | type HttpWorkload =
47 | | EndResponse
48 | | TextResponse of string
49 | | HtmlResponse of String
50 | | FileResponse of string
51 | | JsonResponse of obj
52 |
53 | type HttpHandler = SystemDefaults -> HttpContext -> HttpWorkload
54 | type TaskHttpHandler = SystemDefaults -> HttpContext -> Task
55 |
56 | type ErrorHandler = SystemDefaults -> Exception -> HttpContext -> HttpWorkload option
57 | type TaskErrorHandler = SystemDefaults -> Exception -> HttpContext -> Task
58 |
--------------------------------------------------------------------------------
/src/WebFrame/RouteParts.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.RouteParts
2 |
3 | open Microsoft.AspNetCore.Http
4 |
5 | open WebFrame.Exceptions
6 | open WebFrame.Converters
7 | open WebFrame.RouteTypes
8 |
9 |
10 | type RequestPathProperties ( req: HttpRequest ) =
11 | member val Method = req.Method
12 | member val Protocol = req.Protocol
13 | member val Scheme = req.Scheme
14 | member val Host = req.Host.Host
15 | member val Port = req.Host.Port |> Option.ofNullable
16 | member val PathBase = req.PathBase.Value
17 | member val Path = req.Path.Value
18 | member val QueryString = req.QueryString.Value
19 | member val IsHttps = req.IsHttps
20 |
21 | type RouteParameters ( req: HttpRequest ) =
22 | member _.String ( name: string ) : string option =
23 | match req.RouteValues.TryGetValue name with
24 | | true, v -> Some v
25 | | _ -> None
26 | |> Option.map ( fun i -> i :?> string )
27 |
28 | member this.Get<'T> ( name: string ) ( d: 'T ) : 'T =
29 | name
30 | |> this.Optional
31 | |> Option.defaultValue d
32 |
33 | member this.Required<'T> ( name: string ) : 'T =
34 | name
35 | |> this.Optional<'T>
36 | |> Option.defaultWith ( fun _ -> MissingRequiredRouteParameterException name |> raise )
37 |
38 | member this.Optional<'T> ( name: string ) : 'T option =
39 | name
40 | |> this.String
41 | |> Option.bind convertTo<'T>
42 |
43 | member val Raw = req.RouteValues
44 |
45 | type AllRoutes ( routeDescriptor: Lazy ) =
46 | let mutable rootService = None
47 | member _.All () : RouteDef list =
48 | match rootService with
49 | | None ->
50 | let v = routeDescriptor.Value
51 | rootService <- Some v
52 | v.All ()
53 | | Some v ->
54 | v.All ()
55 |
56 | member _.Optional name : RouteDef option =
57 | match rootService with
58 | | None ->
59 | let v = routeDescriptor.Value
60 | rootService <- Some v
61 | v.TryGet name
62 | | Some v ->
63 | v.TryGet name
64 |
--------------------------------------------------------------------------------
/src/WebFrame/QueryParts.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.QueryParts
2 |
3 | open Microsoft.AspNetCore.Http
4 |
5 | open WebFrame.Exceptions
6 | open WebFrame.Converters
7 |
8 | type QueryParameters ( req: HttpRequest ) =
9 | member this.String ( name: string ) : string option =
10 | name
11 | |> this.AllString
12 | |> Option.bind List.tryHead
13 |
14 | member this.Optional<'T when 'T : equality> ( name: string ) : 'T option =
15 | name
16 | |> this.String
17 | |> Option.bind convertTo<'T>
18 |
19 | member this.Required<'T when 'T : equality> ( name: string ) : 'T =
20 | name
21 | |> this.Optional<'T>
22 | |> Option.defaultWith ( fun _ -> MissingRequiredQueryParameterException name |> raise )
23 |
24 | member this.Get<'T when 'T : equality> ( name: string ) ( d: 'T ) : 'T =
25 | name
26 | |> this.Optional<'T>
27 | |> Option.defaultValue d
28 |
29 | member this.All<'T when 'T : equality> ( name: string ) : 'T list =
30 | name
31 | |> this.AllOptional<'T>
32 | |> Option.defaultValue []
33 |
34 | member this.AllString<'T when 'T : equality> ( name: string ) : string list option =
35 | match req.Query.TryGetValue name with
36 | | true, v -> Some v
37 | | _ -> None
38 | |> Option.map ( fun i -> i.ToArray () |> List.ofArray )
39 |
40 | member this.AllOptional<'T when 'T : equality> ( name: string ) : 'T list option =
41 | name
42 | |> this.AllString
43 | |> Option.map ( List.map convertTo<'T> )
44 | |> Option.bind ( fun i -> if i |> List.contains None then None else Some i )
45 | |> Option.map ( List.map Option.get )
46 | |> Option.bind ( fun i -> if i.Length = 0 then None else Some i )
47 |
48 | member this.AllRequired<'T when 'T : equality> ( name: string ) : 'T list =
49 | name
50 | |> this.AllOptional<'T>
51 | |> Option.defaultWith ( fun _ -> MissingRequiredQueryParameterException name |> raise )
52 |
53 | member this.Count<'T when 'T : equality> ( name: string ) : int =
54 | name
55 | |> this.AllString
56 | |> Option.map List.length
57 | |> Option.defaultValue 0
58 |
59 | member val Raw = req.Query
60 |
--------------------------------------------------------------------------------
/.github/workflows/github-actions.yml:
--------------------------------------------------------------------------------
1 | name: .NET Core
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build_ubuntu:
7 |
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | dotnet-version: [ '6.0.x' ]
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 | - name: Setup dotnet ${{ matrix.dotnet-version }}
16 | uses: actions/setup-dotnet@v1
17 | with:
18 | dotnet-version: ${{ matrix.dotnet-version }}
19 | - name: Install tools
20 | run: dotnet tool restore
21 | - name: Install dependencies
22 | run: dotnet restore
23 | - name: Build Core
24 | run: dotnet build ./src/WebFrame
25 | - name: Build Tests
26 | run: dotnet build ./tests/WebFrame.Tests
27 | - name: Test with the dotnet CLI
28 | run: dotnet test -v m ./tests/WebFrame.Tests
29 | build_windows:
30 |
31 | runs-on: windows-latest
32 | strategy:
33 | matrix:
34 | dotnet-version: [ '6.0.x' ]
35 |
36 | steps:
37 | - uses: actions/checkout@v2
38 | - name: Setup dotnet ${{ matrix.dotnet-version }}
39 | uses: actions/setup-dotnet@v1
40 | with:
41 | dotnet-version: ${{ matrix.dotnet-version }}
42 | - name: Install tools
43 | run: dotnet tool restore
44 | - name: Install dependencies
45 | run: dotnet restore
46 | - name: Build Core
47 | run: dotnet build ./src/WebFrame
48 | - name: Build Tests
49 | run: dotnet build ./tests/WebFrame.Tests
50 | - name: Test with the dotnet CLI
51 | run: dotnet test -v m ./tests/WebFrame.Tests
52 |
53 | build_macos:
54 |
55 | runs-on: macos-latest
56 | strategy:
57 | matrix:
58 | dotnet-version: [ '6.0.x' ]
59 |
60 | steps:
61 | - uses: actions/checkout@v2
62 | - name: Setup dotnet ${{ matrix.dotnet-version }}
63 | uses: actions/setup-dotnet@v1
64 | with:
65 | dotnet-version: ${{ matrix.dotnet-version }}
66 | - name: Install tools
67 | run: dotnet tool restore
68 | - name: Install dependencies
69 | run: dotnet restore
70 | - name: Build Core
71 | run: dotnet build ./src/WebFrame
72 | - name: Build Tests
73 | run: dotnet build ./tests/WebFrame.Tests
74 | - name: Test with the dotnet CLI
75 | run: dotnet test -v m ./tests/WebFrame.Tests
--------------------------------------------------------------------------------
/src/WebFrame/Templating.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.Templating
2 |
3 | open System.IO
4 | open System.Collections.Generic
5 |
6 | open Microsoft.AspNetCore.Hosting
7 |
8 | open Microsoft.Extensions.Logging
9 |
10 | open DotLiquid
11 | open DotLiquid.FileSystems
12 |
13 | open WebFrame.Configuration
14 | open WebFrame.Http
15 | open WebFrame.Exceptions
16 |
17 | type ITemplateCache =
18 | abstract member Path: string
19 | abstract member Value: Template
20 |
21 | type ITemplateRenderer =
22 | abstract member Load: path: string -> unit
23 | abstract member Render: path: string -> o: obj -> HttpWorkload
24 |
25 | type TemplateCache ( logger: ILogger, env: IWebHostEnvironment, path: string ) =
26 | let mutable template: Template option = None
27 |
28 | let load () =
29 | let fileProvider = env.ContentRootFileProvider
30 | let file = fileProvider.GetFileInfo path
31 |
32 | if not file.Exists then
33 | logger.LogError $"DotLiquid template was not found at: {file.PhysicalPath}"
34 | raise ( MissingTemplateException path )
35 |
36 | use stream = file.CreateReadStream ()
37 | use reader = new StreamReader ( stream )
38 |
39 | let t = reader.ReadToEnd () |> Template.Parse
40 | template <- Some t
41 | t
42 |
43 | interface ITemplateCache with
44 | member this.Path = path
45 | member this.Value =
46 | match template with
47 | | Some v -> v
48 | | None -> load ()
49 |
50 | type DotLiquidTemplateService ( defaults: SystemDefaults, rootPath: string, loggerFactory: ILoggerFactory, env: IWebHostEnvironment ) =
51 | let logger =
52 | let loggerName = defaults |> SystemDefaults.getLoggerNameForCategory "DotLiquidTemplateService"
53 | loggerFactory.CreateLogger loggerName
54 | let templates = Dictionary ()
55 |
56 | let load ( path: string ): ITemplateCache =
57 | match templates.TryGetValue path with
58 | | true, v -> v
59 | | _ ->
60 | let t = TemplateCache ( logger, env, path )
61 | templates.[ path ] <- t
62 | t
63 |
64 | do
65 | logger.LogInformation $"Template Root Set to {rootPath}"
66 | Template.FileSystem <- LocalFileSystem rootPath
67 |
68 | interface ITemplateRenderer with
69 | member _.Load ( path: string ) = load path |> ignore
70 | member this.Render ( path: string ) ( o: obj ) =
71 | let c = load path
72 | let t = c.Value
73 |
74 | o |> Hash.FromAnonymousObject |> t.Render |> HtmlResponse
75 |
--------------------------------------------------------------------------------
/src/WebFrame/Configuration.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.Configuration
2 |
3 | open System.Collections.Generic
4 |
5 | open System.Globalization
6 | open Microsoft.Extensions.Logging
7 |
8 | open Newtonsoft.Json
9 | open Newtonsoft.Json.Serialization
10 |
11 | type SystemDefaults = {
12 | SettingsPrefix: string
13 | LoggerPrefix: string
14 | LoggerGlobalName: string
15 | LoggerHostFactory: ILoggerFactory
16 | Args: string []
17 | }
18 |
19 | module SystemDefaults =
20 | let defaultLoggerFactory =
21 | LoggerFactory.Create ( fun l -> l.AddSimpleConsole () |> ignore )
22 | let standard = {
23 | SettingsPrefix = "WebFrame"
24 | LoggerPrefix = "WebFrame"
25 | LoggerGlobalName = "Global"
26 | LoggerHostFactory = defaultLoggerFactory
27 | Args = [||]
28 | }
29 | let defaultWithArgs args = { standard with Args = args }
30 | let getLoggerNameForCategory ( name: string ) ( c: SystemDefaults ) =
31 | let prefix = if c.LoggerPrefix <> "" then $"{c.LoggerPrefix}." else ""
32 | $"{prefix}{name}"
33 | let getGlobalLoggerName ( c: SystemDefaults ) =
34 | c |> getLoggerNameForCategory c.LoggerGlobalName
35 | let getHostLogger ( c: SystemDefaults ) =
36 | let factory = c.LoggerHostFactory
37 | let loggerName = c |> getGlobalLoggerName
38 | factory.CreateLogger loggerName
39 | let toMap ( c: SystemDefaults ) =
40 | let prefix = if c.SettingsPrefix="" then "WebFrame" else c.SettingsPrefix
41 | Map [
42 | $"{prefix}:GlobalLogger:FullName", getGlobalLoggerName c
43 | $"{prefix}:GlobalLogger:Prefix", c.LoggerPrefix
44 | ]
45 |
46 | type IGlobalizationConfig =
47 | abstract DefaultCulture: CultureInfo with get
48 | abstract AllowedCultures: CultureInfo list with get
49 |
50 | type IUserExceptionFilter =
51 | abstract ShowUserException: bool
52 |
53 | type ConfigOverrides ( _defaultConfig: SystemDefaults ) =
54 | let config = Dictionary ()
55 |
56 | let get i =
57 | match config.TryGetValue i with
58 | | true, v -> v
59 | | _ -> ""
60 | let set i v = config.[ i ] <- v
61 |
62 | member _.Item with get i = get i and set i v = config.[ i ] <- v
63 | member _.ConnectionStrings with get i = get $"ConnectionStrings:{i}" and set i v = set $"ConnectionStrings:{i}" v
64 |
65 | member val Raw = config
66 |
67 | // Some Json Serialization Configs
68 | type IJsonSerializationService =
69 | abstract Settings: JsonSerializerSettings
70 |
71 | type IJsonDeserializationService =
72 | abstract Settings: JsonSerializerSettings
73 |
74 | type RequireAllPropertiesContractResolver () =
75 | inherit DefaultContractResolver ()
76 |
77 | // Code samples are taken from:
78 | // https://stackoverflow.com/questions/29655502/json-net-require-all-properties-on-deserialization/29660550
79 |
80 | override this.CreateProperty ( memberInfo, serialization ) =
81 | let prop = base.CreateProperty ( memberInfo, serialization )
82 | let isRequired =
83 | not prop.PropertyType.IsGenericType || prop.PropertyType.GetGenericTypeDefinition () <> typedefof>
84 | if isRequired then prop.Required <- Required.Always
85 | prop
86 |
--------------------------------------------------------------------------------
/src/WebFrame/Exceptions.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.Exceptions
2 |
3 | open System
4 |
5 | // Base exception classes
6 | type InputException ( msg: string ) = inherit Exception ( msg )
7 | type AuthException ( msg: string ) = inherit Exception ( msg )
8 | type ServerException ( msg: string ) = inherit Exception ( msg )
9 |
10 | // Exceptions raised in response to incorrect user input
11 | // Should result in 400 responses
12 | type MissingRequiredRouteParameterException ( parameterName: string ) =
13 | inherit InputException $"Could not retrieve a required parameter named '%s{parameterName}' from the route values."
14 |
15 | type MissingRequiredQueryParameterException ( parameterName: string ) =
16 | inherit InputException $"Could not retrieve a required parameter named '%s{parameterName}' from the query values."
17 |
18 | type MissingRequiredHeaderException ( headerName: string ) =
19 | inherit InputException $"Could not retrieve a required header named '%s{headerName}' from the request."
20 |
21 | type MissingRequiredCookieException ( cookieName: string ) =
22 | inherit InputException $"Could not retrieve a required cookie named '%s{cookieName}' from the request."
23 |
24 | type MissingRequiredFormFieldException ( fieldName: string ) =
25 | inherit InputException $"Could not retrieve a required form field named '%s{fieldName}' from the request."
26 |
27 | type MissingRequiredFormFileException ( fileName: string ) =
28 | inherit InputException $"Could not retrieve a required form file named '%s{fileName}' from the request."
29 |
30 | type MissingRequiredFormException () =
31 | inherit InputException $"Could not retrieve a form body from the request."
32 |
33 | type MissingRequiredJsonFieldException ( fieldName: string ) =
34 | inherit InputException $"Could not retrieve a required json field named '%s{fieldName}' from the request."
35 |
36 | type MissingRequiredJsonException () =
37 | inherit InputException $"Could not retrieve an expected json body from the request."
38 |
39 | // Unspecified input validation exception
40 | type GenericInputException ( msg: string ) = inherit InputException ( msg )
41 |
42 | type NotAuthneticatedException () = inherit AuthException $"The user was not authenticated"
43 |
44 | // Exceptions raised in response to an incorrect app setup
45 | // Generally, should result in an app breaking on startup
46 | // However, it is not always possible
47 | type MissingRequiredDependencyException ( dependencyName: string ) =
48 | inherit ServerException $"Could not retrieve an object of type '%s{dependencyName}' from ASP.NET Core's dependency container."
49 |
50 | type MissingAuthenticationException () =
51 | inherit ServerException "Could not find authentication methods"
52 |
53 | type MissingRequiredConfigException ( configName: string ) =
54 | inherit ServerException $"Could not read a property named '%s{configName}' from ASP.NET Core's configuration container."
55 |
56 | type DuplicateRouteException ( routePattern: string ) =
57 | inherit ServerException $"Could not register a route '{routePattern}'. The route is already registered."
58 |
59 | type DuplicateModuleException ( name: string ) =
60 | inherit ServerException $"Could not register a module '{name}'. The module with such a name is already registered."
61 |
62 | type HostNotReadyException () =
63 | inherit ServerException $"Cannot access the IHost before it is built."
64 |
65 | type MissingTemplateException ( path: string ) =
66 | inherit ServerException $"The template at \"{path}\" was not found."
67 |
68 | /// Unspecified server setup exception
69 | type GenericServerException ( msg: string ) = inherit ServerException ( msg )
70 |
--------------------------------------------------------------------------------
/src/WebFrame/Services.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.Services
2 |
3 | open System.Threading.Tasks
4 |
5 | open Microsoft.AspNetCore.Http
6 |
7 | open Microsoft.Extensions.Logging
8 |
9 | open WebFrame.ConfigParts
10 | open WebFrame.Configuration
11 | open WebFrame.GlobalizationParts
12 | open WebFrame.Http
13 | open WebFrame.BodyParts
14 | open WebFrame.CookieParts
15 | open WebFrame.HeaderParts
16 | open WebFrame.Logging
17 | open WebFrame.QueryParts
18 | open WebFrame.RouteParts
19 | open WebFrame.RouteTypes
20 | open WebFrame.ServicesParts
21 | open WebFrame.Templating
22 |
23 | type RequestServices ( ctx: HttpContext, defaults: SystemDefaults ) as this =
24 | let endpoint = ctx.GetEndpoint ()
25 | let metadata = endpoint.Metadata
26 | let routeDescription = metadata.GetMetadata ()
27 |
28 | member val Context = ctx
29 | member val Path = RequestPathProperties ctx.Request
30 | member val Route = RouteParameters ctx.Request
31 | member val Query = QueryParameters ctx.Request
32 | member val Headers = Headers ( ctx.Request, ctx.Response )
33 | member val Cookies = Cookies ( ctx.Request, ctx.Response )
34 | member val Body = Body ( ctx.Request, lazy ( this.Services.Required () ) )
35 | member val Services: GenericServiceProvider = GenericServiceProvider ctx.RequestServices
36 |
37 | member val AppRoutes = AllRoutes ( lazy ( this.Services.Required () ) )
38 | member val Globalization = Globalization ( ctx, lazy ( this.Services.Required () ) )
39 |
40 | member _.Redirect ( url, permanent ) =
41 | ctx.Response.Redirect ( url, permanent )
42 | EndResponse
43 |
44 | member val Config = RuntimeConfigs ( lazy ( this.Services.Required () ), lazy ( this.Services.Required () ) )
45 | member this.Redirect url = this.Redirect ( url, false )
46 | member _.EndResponse () = EndResponse
47 | member _.EndResponse ( t: string ) = TextResponse t
48 | member _.EndResponse ( j: obj ) = JsonResponse j
49 | member _.File n = FileResponse n
50 | member this.File ( name: string, contentType: string ) =
51 | this.ContentType <- contentType
52 | this.File name
53 | member this.Page ( path: string ) ( o: obj ) =
54 | let renderer = this.Services.Required ()
55 | renderer.Render path o
56 | member val Endpoint = endpoint
57 | member val RouteDescription = routeDescription
58 | member _.StatusCode with set v = ctx.Response.StatusCode <- v
59 | member this.ContentType
60 | with get () = this.Headers.Get "Content-Type" ""
61 | and set v = this.Headers.Set "Content-Type" [ v ]
62 | member val Log =
63 | let category = defaults |> SystemDefaults.getLoggerNameForCategory routeDescription.Name
64 | Logger ( lazy ( this.Services.Required () ), category )
65 | member this.LoggerFor ( categoryName: string ) =
66 | let f = this.Services.Required ()
67 | f.CreateLogger categoryName
68 | member _.EnableBuffering () = ctx.Request.EnableBuffering ()
69 |
70 | type ServicedHandler = RequestServices -> HttpWorkload
71 | type TaskServicedHandler = RequestServices -> Task
72 |
73 | type HandlerSetup = ServicedHandler -> HttpHandler
74 | type TaskHandlerSetup = TaskServicedHandler -> TaskHttpHandler
75 |
76 | type ServicedErrorHandler<'T when 'T :> exn> = 'T -> RequestServices -> HttpWorkload
77 | type ServicedTaskErrorHandler<'T when 'T :> exn> = 'T -> RequestServices -> Task
78 |
79 | type ErrorHandlerSetup<'T when 'T :> exn> = ServicedErrorHandler<'T> -> ErrorHandler
80 | type TaskErrorHandlerSetup<'T when 'T :> exn> = ServicedTaskErrorHandler<'T> -> TaskErrorHandler
81 |
82 | module TaskServicedHandler =
83 | let toTaskHttpHandler: TaskHandlerSetup = fun s ->
84 | fun config ctx -> ( ctx, config ) |> RequestServices |> s
85 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: .NET Core Release
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | check_tag:
9 | runs-on: ubuntu-latest
10 | outputs:
11 | release_all: ${{ steps.tag.outputs.release_all }}
12 | release_core: ${{ steps.tag.outputs.release_core }}
13 | release_templates: ${{ steps.tag.outputs.release_templates }}
14 | release_version: ${{ steps.version.outputs.release_version }}
15 | steps:
16 | - name: Check the tag ${{ github.ref }}
17 | id: tag
18 | run: |
19 | if [[ ${{ github.ref }} =~ refs\/tags\/all@v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
20 | echo "::set-output name=release_all::true"
21 | else
22 | echo "::set-output name=release_all::false"
23 | fi
24 | if [[ ${{ github.ref }} =~ refs\/tags\/core@v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
25 | echo "::set-output name=release_core::true"
26 | else
27 | echo "::set-output name=release_core::false"
28 | fi
29 | if [[ ${{ github.ref }} =~ refs\/tags\/templates@v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
30 | echo "::set-output name=release_templates::true"
31 | else
32 | echo "::set-output name=release_templates::false"
33 | fi
34 | - name: Get the version
35 | id: version
36 | run: echo "::set-output name=release_version::${GITHUB_REF#refs/tags/*@v}"
37 | release_core:
38 | needs: check_tag
39 | if: (needs.check_tag.outputs.release_all == 'true') || (needs.check_tag.outputs.release_core == 'true')
40 | runs-on: ubuntu-latest
41 | env:
42 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
43 | strategy:
44 | matrix:
45 | dotnet-version: [ '6.0.x' ]
46 |
47 | steps:
48 | - uses: actions/checkout@v2
49 | - name: Setup dotnet ${{ matrix.dotnet-version }}
50 | uses: actions/setup-dotnet@v1
51 | with:
52 | dotnet-version: ${{ matrix.dotnet-version }}
53 | - name: Install tools
54 | run: dotnet tool restore
55 | - name: Install dependencies
56 | run: dotnet restore
57 | - name: Build Core
58 | run: dotnet build ./src/WebFrame
59 | - name: Build Tests
60 | run: dotnet build ./tests/WebFrame.Tests
61 | - name: Test with the dotnet CLI
62 | run: dotnet test -v m ./tests/WebFrame.Tests
63 | - run: dotnet build --configuration Release ./src/WebFrame/WebFrame.fsproj
64 | - name: Create the package
65 | run: dotnet paket pack --template ./src/WebFrame/paket.template --specific-version RussBaz.WebFrame ${{ needs.check_tag.outputs.release_version }} --build-config Release .
66 | - name: Publish the package to nuget
67 | run: dotnet nuget push ./*.nupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json
68 | - name: Remove nuget packages
69 | run: rm -f *.nupkg
70 | release_templates:
71 | needs: check_tag
72 | if: (needs.check_tag.outputs.release_all == 'true') || (needs.check_tag.outputs.release_templates == 'true')
73 | runs-on: ubuntu-latest
74 | env:
75 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
76 | strategy:
77 | matrix:
78 | dotnet-version: [ '6.0.x' ]
79 |
80 | steps:
81 | - uses: actions/checkout@v2
82 | - name: Setup dotnet ${{ matrix.dotnet-version }}
83 | uses: actions/setup-dotnet@v1
84 | with:
85 | dotnet-version: ${{ matrix.dotnet-version }}
86 | - name: Install tools
87 | run: dotnet tool restore
88 | - name: Install dependencies
89 | run: dotnet restore
90 | - run: dotnet build --configuration Release ./templates/templatepack.fsproj
91 | - name: Create the package
92 | run: dotnet pack ./templates/templatepack.fsproj -c Release -p:PackageVersion=${{ needs.check_tag.outputs.release_version }} -o .
93 | - name: Publish the package to nuget
94 | run: dotnet nuget push ./*.nupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json
95 | - name: Remove nuget packages
96 | run: rm -f *.nupkg
--------------------------------------------------------------------------------
/src/WebFrame/RouteTypes.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.RouteTypes
2 |
3 | open System
4 |
5 | open System.Collections.Generic
6 | open Microsoft.AspNetCore.Authorization
7 | open Microsoft.AspNetCore.Builder
8 | open Microsoft.AspNetCore.Cors.Infrastructure
9 |
10 | open WebFrame.Http
11 |
12 | type RoutePattern = {
13 | Path: string
14 | Methods: Set
15 | }
16 | with
17 | static member ( + ) ( v1: RoutePattern, v2: string ) =
18 | { v1 with Path = v1.Path + v2 }
19 | static member ( + ) ( v1: string, v2: RoutePattern ) =
20 | { v2 with Path = v1 + v2.Path }
21 | override this.ToString () =
22 | let s = this.Methods |> Seq.map ( fun i -> i.ToString () ) |> Seq.sort |> String.concat "; "
23 | $"[ {s} ]"
24 |
25 | type AuthorizationDef =
26 | | NoneAuth
27 | | AnonAuth
28 | | DefaultAuth
29 | | PolicyAuth of string list
30 | | DataAuth of IAuthorizeData list
31 |
32 | type CORSDef =
33 | | NoneCORS
34 | | PolicyCORS of string
35 | | NewPolicyCORS of ( CorsPolicyBuilder->unit )
36 |
37 | type FieldContent =
38 | | ValueContent
39 | type FieldDef =
40 | | RequiredField
41 | | OptionalField
42 | | DefaultField
43 |
44 | type RouteParamDef = {
45 | Name: string
46 | ExpectedType: Type
47 | Description: string
48 | Constraints: string
49 | Required: bool
50 | }
51 |
52 | type QueryParamDef = {
53 | Name: string
54 | ExpectedType: Type
55 | Description: string
56 | Constraints: string
57 | Required: bool
58 | List: bool
59 | }
60 |
61 | type HeaderDef = {
62 | Name: string
63 | Description: string
64 | Constraints: string
65 | Required: bool
66 | List: bool
67 | }
68 |
69 | type CookieDef = {
70 | Name: string
71 | Description: string
72 | Constraints: string
73 | Required: bool
74 | }
75 |
76 | type FormFieldDef = {
77 | Name: string
78 | ExpectedType: Type
79 | Description: string
80 | Constraints: string
81 | Required: bool
82 | List: bool
83 | }
84 |
85 | type JsonBodyDef = {
86 | ExpectedType: Type
87 | }
88 |
89 | type BodyDef =
90 | | FormBodyExpected of FormFieldDef list
91 | | JsonBodyExpected
92 | | BinaryBodyExpected
93 |
94 | type RouteDef = {
95 | Name: string
96 | Pattern: RoutePattern
97 | Description: string
98 | Auth: AuthorizationDef
99 | CORS: CORSDef
100 | Host: string list
101 | PreConfig: IEndpointConventionBuilder->IEndpointConventionBuilder
102 | PostConfig: IEndpointConventionBuilder->IEndpointConventionBuilder
103 | HttpHandler: TaskHttpHandler
104 | ErrorHandlers: TaskErrorHandler list
105 | Metadata: obj list
106 | }
107 |
108 | module RoutePattern =
109 | let create ( path: string ) ( methods: HttpMethod list ) =
110 | {
111 | Path = path
112 | Methods = methods |> Set.ofList
113 | }
114 |
115 | module RouteDef =
116 | let create path = {
117 | Name = ""
118 | Pattern = path
119 | Description = ""
120 | Auth = NoneAuth
121 | CORS = NoneCORS
122 | Host = []
123 | PreConfig = id
124 | PostConfig = id
125 | HttpHandler = fun _ _ -> task { return EndResponse }
126 | ErrorHandlers = []
127 | Metadata = [] }
128 |
129 | let name ( n: string ) ( r: RouteDef ) = { r with Name = n }
130 | let prefixWith ( p: string ) ( r: RouteDef ) = { r with Pattern = p + r.Pattern }
131 | let description ( d: string ) ( r: RouteDef ) = { r with Description = d }
132 | let authorization ( a: AuthorizationDef ) ( r: RouteDef ) = { r with Auth = a }
133 | let cors ( c: CORSDef ) ( r: RouteDef ) = { r with CORS = c }
134 | let host ( h: string list ) ( r: RouteDef ) = { r with Host = h }
135 | let preConfig ( c: IEndpointConventionBuilder->IEndpointConventionBuilder ) ( r: RouteDef ) =
136 | { r with PreConfig = c }
137 | let postConfig ( c: IEndpointConventionBuilder->IEndpointConventionBuilder ) ( r: RouteDef ) =
138 | { r with PostConfig = c }
139 | let handler ( h: TaskHttpHandler ) ( r: RouteDef ) = { r with HttpHandler = h }
140 | let onError ( h: TaskErrorHandler) ( r: RouteDef ) = { r with ErrorHandlers = h :: r.ErrorHandlers }
141 | let onErrors ( h: TaskErrorHandler list ) ( r: RouteDef ) =
142 | let h = h |> List.rev
143 | let h = List.append h r.ErrorHandlers
144 | { r with ErrorHandlers = h }
145 | let metadata ( d: obj ) ( r: RouteDef ) = { r with Metadata = d :: r.Metadata }
146 | let createWithHandler p h = p |> create |> handler h |> name ( p.ToString () )
147 | let generate ( path: string ) ( methods: HttpMethod list ) =
148 | methods
149 | |> RoutePattern.create path
150 | let apply ( f: RouteDef -> RouteDef ) ( d: ( RoutePattern * RouteDef ) list ) =
151 | d
152 | |> List.map ( fun ( p, d ) -> p, f d )
153 |
154 | type Routes = Dictionary
155 |
156 | type RouteDescription ( route: RouteDef ) =
157 | member val Name = route.Name
158 | member val Pattern = route.Pattern
159 | member val Description = route.Description
160 |
161 | type IRouteDescriptorService =
162 | abstract member All: unit -> RouteDef list
163 | abstract member TryGet: RoutePattern -> RouteDef option
164 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET Core
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # Tye
66 | .tye/
67 |
68 | # ASP.NET Scaffolding
69 | ScaffoldingReadMe.txt
70 |
71 | # StyleCop
72 | StyleCopReport.xml
73 |
74 | # Files built by Visual Studio
75 | *_i.c
76 | *_p.c
77 | *_h.h
78 | *.ilk
79 | *.meta
80 | *.obj
81 | *.iobj
82 | *.pch
83 | *.pdb
84 | *.ipdb
85 | *.pgc
86 | *.pgd
87 | *.rsp
88 | *.sbr
89 | *.tlb
90 | *.tli
91 | *.tlh
92 | *.tmp
93 | *.tmp_proj
94 | *_wpftmp.csproj
95 | *.log
96 | *.vspscc
97 | *.vssscc
98 | .builds
99 | *.pidb
100 | *.svclog
101 | *.scc
102 |
103 | # Chutzpah Test files
104 | _Chutzpah*
105 |
106 | # Visual C++ cache files
107 | ipch/
108 | *.aps
109 | *.ncb
110 | *.opendb
111 | *.opensdf
112 | *.sdf
113 | *.cachefile
114 | *.VC.db
115 | *.VC.VC.opendb
116 |
117 | # Visual Studio profiler
118 | *.psess
119 | *.vsp
120 | *.vspx
121 | *.sap
122 |
123 | # Visual Studio Trace Files
124 | *.e2e
125 |
126 | # TFS 2012 Local Workspace
127 | $tf/
128 |
129 | # Guidance Automation Toolkit
130 | *.gpState
131 |
132 | # ReSharper is a .NET coding add-in
133 | _ReSharper*/
134 | *.[Rr]e[Ss]harper
135 | *.DotSettings.user
136 |
137 | # TeamCity is a build add-in
138 | _TeamCity*
139 |
140 | # DotCover is a Code Coverage Tool
141 | *.dotCover
142 |
143 | # AxoCover is a Code Coverage Tool
144 | .axoCover/*
145 | !.axoCover/settings.json
146 |
147 | # Coverlet is a free, cross platform Code Coverage Tool
148 | coverage*.json
149 | coverage*.xml
150 | coverage*.info
151 |
152 | # Visual Studio code coverage results
153 | *.coverage
154 | *.coveragexml
155 |
156 | # NCrunch
157 | _NCrunch_*
158 | .*crunch*.local.xml
159 | nCrunchTemp_*
160 |
161 | # MightyMoose
162 | *.mm.*
163 | AutoTest.Net/
164 |
165 | # Web workbench (sass)
166 | .sass-cache/
167 |
168 | # Installshield output folder
169 | [Ee]xpress/
170 |
171 | # DocProject is a documentation generator add-in
172 | DocProject/buildhelp/
173 | DocProject/Help/*.HxT
174 | DocProject/Help/*.HxC
175 | DocProject/Help/*.hhc
176 | DocProject/Help/*.hhk
177 | DocProject/Help/*.hhp
178 | DocProject/Help/Html2
179 | DocProject/Help/html
180 |
181 | # Click-Once directory
182 | publish/
183 |
184 | # Publish Web Output
185 | *.[Pp]ublish.xml
186 | *.azurePubxml
187 | # Note: Comment the next line if you want to checkin your web deploy settings,
188 | # but database connection strings (with potential passwords) will be unencrypted
189 | *.pubxml
190 | *.publishproj
191 |
192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
193 | # checkin your Azure Web App publish settings, but sensitive information contained
194 | # in these scripts will be unencrypted
195 | PublishScripts/
196 |
197 | # NuGet Packages
198 | *.nupkg
199 | # NuGet Symbol Packages
200 | *.snupkg
201 | # The packages folder can be ignored because of Package Restore
202 | **/[Pp]ackages/*
203 | # except build/, which is used as an MSBuild target.
204 | !**/[Pp]ackages/build/
205 | # Uncomment if necessary however generally it will be regenerated when needed
206 | #!**/[Pp]ackages/repositories.config
207 | # NuGet v3's project.json files produces more ignorable files
208 | *.nuget.props
209 | *.nuget.targets
210 |
211 | # Microsoft Azure Build Output
212 | csx/
213 | *.build.csdef
214 |
215 | # Microsoft Azure Emulator
216 | ecf/
217 | rcf/
218 |
219 | # Windows Store app package directories and files
220 | AppPackages/
221 | BundleArtifacts/
222 | Package.StoreAssociation.xml
223 | _pkginfo.txt
224 | *.appx
225 | *.appxbundle
226 | *.appxupload
227 |
228 | # Visual Studio cache files
229 | # files ending in .cache can be ignored
230 | *.[Cc]ache
231 | # but keep track of directories ending in .cache
232 | !?*.[Cc]ache/
233 |
234 | # Others
235 | ClientBin/
236 | ~$*
237 | *~
238 | *.dbmdl
239 | *.dbproj.schemaview
240 | *.jfm
241 | *.pfx
242 | *.publishsettings
243 | orleans.codegen.cs
244 |
245 | # Including strong name files can present a security risk
246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
247 | #*.snk
248 |
249 | # Since there are multiple workflows, uncomment next line to ignore bower_components
250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
251 | #bower_components/
252 |
253 | # RIA/Silverlight projects
254 | Generated_Code/
255 |
256 | # Backup & report files from converting an old project file
257 | # to a newer Visual Studio version. Backup files are not needed,
258 | # because we have git ;-)
259 | _UpgradeReport_Files/
260 | Backup*/
261 | UpgradeLog*.XML
262 | UpgradeLog*.htm
263 | ServiceFabricBackup/
264 | *.rptproj.bak
265 |
266 | # SQL Server files
267 | *.mdf
268 | *.ldf
269 | *.ndf
270 |
271 | # Business Intelligence projects
272 | *.rdl.data
273 | *.bim.layout
274 | *.bim_*.settings
275 | *.rptproj.rsuser
276 | *- [Bb]ackup.rdl
277 | *- [Bb]ackup ([0-9]).rdl
278 | *- [Bb]ackup ([0-9][0-9]).rdl
279 |
280 | # Microsoft Fakes
281 | FakesAssemblies/
282 |
283 | # GhostDoc plugin setting file
284 | *.GhostDoc.xml
285 |
286 | # Node.js Tools for Visual Studio
287 | .ntvs_analysis.dat
288 | node_modules/
289 |
290 | # Visual Studio 6 build log
291 | *.plg
292 |
293 | # Visual Studio 6 workspace options file
294 | *.opt
295 |
296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
297 | *.vbw
298 |
299 | # Visual Studio LightSwitch build output
300 | **/*.HTMLClient/GeneratedArtifacts
301 | **/*.DesktopClient/GeneratedArtifacts
302 | **/*.DesktopClient/ModelManifest.xml
303 | **/*.Server/GeneratedArtifacts
304 | **/*.Server/ModelManifest.xml
305 | _Pvt_Extensions
306 |
307 | # Paket dependency manager
308 | .paket/paket.exe
309 | paket-files/
310 |
311 | # FAKE - F# Make
312 | .fake/
313 |
314 | # CodeRush personal settings
315 | .cr/personal
316 |
317 | # Python Tools for Visual Studio (PTVS)
318 | __pycache__/
319 | *.pyc
320 |
321 | # Cake - Uncomment if you are using it
322 | # tools/**
323 | # !tools/packages.config
324 |
325 | # Tabs Studio
326 | *.tss
327 |
328 | # Telerik's JustMock configuration file
329 | *.jmconfig
330 |
331 | # BizTalk build output
332 | *.btp.cs
333 | *.btm.cs
334 | *.odx.cs
335 | *.xsd.cs
336 |
337 | # OpenCover UI analysis results
338 | OpenCover/
339 |
340 | # Azure Stream Analytics local run output
341 | ASALocalRun/
342 |
343 | # MSBuild Binary and Structured Log
344 | *.binlog
345 |
346 | # NVidia Nsight GPU debugger configuration file
347 | *.nvuser
348 |
349 | # MFractors (Xamarin productivity tool) working folder
350 | .mfractor/
351 |
352 | # Local History for Visual Studio
353 | .localhistory/
354 |
355 | # BeatPulse healthcheck temp database
356 | healthchecksdb
357 |
358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
359 | MigrationBackup/
360 |
361 | # Ionide (cross platform F# VS Code tools) working folder
362 | .ionide/
363 |
364 | # Fody - auto-generated XML schema
365 | FodyWeavers.xsd
366 |
367 | ##
368 | ## Visual studio for Mac
369 | ##
370 |
371 |
372 | # globs
373 | Makefile.in
374 | *.userprefs
375 | *.usertasks
376 | config.make
377 | config.status
378 | aclocal.m4
379 | install-sh
380 | autom4te.cache/
381 | *.tar.gz
382 | tarballs/
383 | test-results/
384 |
385 | # Mac bundle stuff
386 | *.dmg
387 | *.app
388 |
389 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
390 | # General
391 | .DS_Store
392 | .AppleDouble
393 | .LSOverride
394 |
395 | # Icon must end with two \r
396 | Icon
397 |
398 |
399 | # Thumbnails
400 | ._*
401 |
402 | # Files that might appear in the root of a volume
403 | .DocumentRevisions-V100
404 | .fseventsd
405 | .Spotlight-V100
406 | .TemporaryItems
407 | .Trashes
408 | .VolumeIcon.icns
409 | .com.apple.timemachine.donotpresent
410 |
411 | # Directories potentially created on remote AFP share
412 | .AppleDB
413 | .AppleDesktop
414 | Network Trash Folder
415 | Temporary Items
416 | .apdisk
417 |
418 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
419 | # Windows thumbnail cache files
420 | Thumbs.db
421 | ehthumbs.db
422 | ehthumbs_vista.db
423 |
424 | # Dump file
425 | *.stackdump
426 |
427 | # Folder config file
428 | [Dd]esktop.ini
429 |
430 | # Recycle Bin used on file shares
431 | $RECYCLE.BIN/
432 |
433 | # Windows Installer files
434 | *.cab
435 | *.msi
436 | *.msix
437 | *.msm
438 | *.msp
439 |
440 | # Windows shortcuts
441 | *.lnk
442 |
443 | # JetBrains Rider
444 | .idea/
445 | *.sln.iml
446 |
447 | ##
448 | ## Visual Studio Code
449 | ##
450 | .vscode/*
451 | !.vscode/settings.json
452 | !.vscode/tasks.json
453 | !.vscode/launch.json
454 | !.vscode/extensions.json
455 |
--------------------------------------------------------------------------------
/src/WebFrame/BodyParts.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.BodyParts
2 |
3 | open System
4 | open System.IO
5 | open System.Text
6 | open System.Threading.Tasks
7 |
8 | open Microsoft.AspNetCore.Http
9 | open Microsoft.Net.Http.Headers
10 | open Microsoft.Extensions.Primitives
11 |
12 | open Newtonsoft.Json
13 | open Newtonsoft.Json.Linq
14 |
15 | open WebFrame.Configuration
16 | open WebFrame.Converters
17 | open WebFrame.Exceptions
18 |
19 |
20 | type FormFiles ( files: IFormFileCollection ) =
21 | member _.All () = files |> Seq.toList
22 | member _.Required ( fileName: string ) =
23 | match files.GetFile fileName with
24 | | null -> raise ( MissingRequiredFormFileException fileName )
25 | | f -> f
26 | member this.Optional ( fileName: string ) =
27 | try
28 | this.Required fileName |> Some
29 | with
30 | | :? MissingRequiredFormFileException -> None
31 |
32 | member val Count = files.Count
33 |
34 | type FormEncodedBody ( req: HttpRequest ) =
35 | let mutable form: IFormCollection option = None
36 | let mutable files: FormFiles option = None
37 |
38 | let getForm () =
39 | match form with
40 | | Some v -> v
41 | | None ->
42 | if req.HasFormContentType then
43 | form <- Some req.Form
44 | else
45 | raise ( MissingRequiredFormException () )
46 |
47 | req.Form
48 |
49 | let getFiles () =
50 | match files with
51 | | Some v -> v
52 | | None ->
53 | let f = getForm ()
54 | let f = FormFiles f.Files
55 |
56 | files <- Some f
57 |
58 | f
59 |
60 | member this.String ( name: string ) : string option =
61 | name |> this.Optional
62 |
63 | member this.Required<'T when 'T : equality> ( name: string ) : 'T =
64 | name
65 | |> this.Optional<'T>
66 | |> Option.defaultWith ( fun _ -> raise ( MissingRequiredFormFieldException name ) )
67 |
68 | member this.Optional<'T when 'T : equality> ( name: string ) : 'T option =
69 | name
70 | |> this.AllString
71 | |> Option.bind List.tryHead
72 | |> Option.bind convertTo<'T>
73 |
74 | member this.Get<'T when 'T : equality> ( name: string ) ( d: 'T ) : 'T =
75 | name
76 | |> this.Optional<'T>
77 | |> Option.defaultValue d
78 |
79 | member this.AllString ( name: string ) : string list option =
80 | let form = getForm ()
81 |
82 | match form.TryGetValue name with
83 | | true, v -> Some v
84 | | _ -> None
85 | |> Option.map ( fun i -> i.ToArray () |> List.ofArray )
86 |
87 | member this.All<'T when 'T : equality> ( name: string ) : 'T list =
88 | name
89 | |> this.AllOptional<'T>
90 | |> Option.defaultValue []
91 |
92 | member this.AllRequired<'T when 'T : equality> ( name: string ) : 'T list =
93 | name
94 | |> this.AllOptional<'T>
95 | |> Option.defaultWith ( fun _ -> raise ( MissingRequiredFormFieldException name ) )
96 |
97 | member this.AllOptional<'T when 'T : equality> ( name: string ) : 'T list option =
98 | name
99 | |> this.AllString
100 | |> Option.map ( List.map convertTo<'T> )
101 | |> Option.bind ( fun i -> if i |> List.contains None then None else Some i )
102 | |> Option.map ( List.map Option.get )
103 | |> Option.bind ( fun i -> if i.Length = 0 then None else Some i )
104 |
105 | member this.Count ( name: string ) : int =
106 | name
107 | |> this.All<_>
108 | |> List.length
109 |
110 | member _.Raw with get () = try getForm () |> Some with | :? MissingRequiredFormException -> None
111 | member _.Files with get () = try getFiles () |> Some with | :? MissingRequiredFormException -> None
112 | member val IsPresent = req.HasFormContentType
113 |
114 | type JsonEncodedBody ( req: HttpRequest, settings: Lazy ) =
115 | let mutable unknownEncoding = false
116 | let mutable json: JObject option = None
117 | let jsonSerializer = lazy ( JsonSerializer.CreateDefault settings.Value )
118 |
119 | let jsonCharset =
120 | match MediaTypeHeaderValue.TryParse ( StringSegment req.ContentType ) with
121 | | true, v ->
122 | if v.MediaType.Equals ( "application/json", StringComparison.OrdinalIgnoreCase ) then
123 | Some v.Charset
124 | elif v.Suffix.Equals ( "json", StringComparison.OrdinalIgnoreCase ) then
125 | Some v.Charset
126 | else
127 | None
128 | | _ ->
129 | None
130 |
131 | let jsonEncoding =
132 | match jsonCharset with
133 | | Some c ->
134 | try
135 | if c.Equals ( "utf-8", StringComparison.OrdinalIgnoreCase ) then
136 | Encoding.UTF8 |> Some
137 | elif c.HasValue then
138 | Encoding.GetEncoding c.Value |> Some
139 | else
140 | None
141 | with
142 | | _ ->
143 | unknownEncoding <- true
144 | None
145 | | None -> None
146 |
147 | let notJsonContentType = jsonCharset.IsNone || unknownEncoding
148 |
149 | let getJson () = task {
150 | if notJsonContentType then raise ( MissingRequiredJsonException () )
151 |
152 | match json with
153 | | Some v -> return v
154 | | None ->
155 | let en = jsonEncoding |> Option.defaultValue Encoding.UTF8
156 |
157 | use br = new StreamReader ( req.Body, en )
158 |
159 | let! body = br.ReadToEndAsync ()
160 |
161 | try
162 | let v = JObject.Parse body
163 |
164 | json <- Some v
165 |
166 | return v
167 | with
168 | | :? JsonSerializationException -> return raise ( MissingRequiredJsonException () )
169 | }
170 |
171 | member private _.ReadJson<'T> () = task {
172 | let! j = getJson ()
173 |
174 | use tr = new JTokenReader ( j )
175 |
176 | try
177 | return jsonSerializer.Value.Deserialize<'T> tr
178 | with
179 | | :? JsonSerializationException -> return raise ( MissingRequiredJsonException () )
180 | }
181 |
182 | member private _.GetField<'T> ( jsonPath: string ) = task {
183 | let! j = getJson ()
184 | let token = j.SelectToken jsonPath
185 | return token.ToObject<'T> ()
186 | }
187 |
188 | member private _.GetFields<'T> ( jsonPath: string ) = task {
189 | let! j = getJson ()
190 |
191 | return
192 | jsonPath
193 | |> j.SelectTokens
194 | |> Seq.map ( fun i -> i.ToObject<'T> () )
195 | |> List.ofSeq
196 | }
197 |
198 | member this.Exact<'T> () : Task<'T> = this.ReadJson<'T> ()
199 |
200 | member this.String ( path: string ) : Task = task {
201 | return! this.Optional path
202 | }
203 |
204 | member this.Get<'T> ( path: string ) ( d: 'T ) : Task<'T> = task {
205 | let! v = this.Optional<'T> path
206 | return v |> Option.defaultValue d
207 | }
208 |
209 | member this.Required<'T> ( path: string ) : Task<'T> = task {
210 | try
211 | return! this.GetField<'T> path
212 | with
213 | | :? NullReferenceException -> return ( raise ( MissingRequiredJsonFieldException path ) )
214 | | :? MissingRequiredJsonException as ex -> return ( raise ex )
215 | | :? MissingRequiredJsonFieldException as ex -> return ( raise ex )
216 | }
217 |
218 | member this.Optional<'T> ( path: string ) : Task<'T option> = task {
219 | try
220 | let! r = this.GetField<'T> path
221 | return Some r
222 | with
223 | | :? NullReferenceException -> return None
224 | | :? MissingRequiredJsonException -> return None
225 | | :? MissingRequiredJsonFieldException -> return None
226 | }
227 |
228 | member this.All<'T> ( path: string ) : Task<'T list> = task {
229 | let! data = path |> this.AllOptional
230 |
231 | return data |> Option.defaultValue []
232 | }
233 |
234 | member this.AllString ( path: string ) = this.All path
235 |
236 | member this.AllRequired<'T> ( path: string ) = task {
237 | try
238 | return! this.GetFields<'T> path
239 | with
240 | | :? NullReferenceException -> return ( raise ( MissingRequiredJsonFieldException path ) )
241 | | :? MissingRequiredJsonException as ex -> return ( raise ex )
242 | | :? MissingRequiredJsonFieldException as ex -> return ( raise ex )
243 | }
244 |
245 | member this.AllOptional<'T> ( path: string ) : Task<'T list option> = task {
246 | try
247 | let! r = this.GetFields<'T> path
248 | return Some r
249 | with
250 | | :? NullReferenceException -> return None
251 | | :? MissingRequiredJsonException -> return None
252 | | :? MissingRequiredJsonFieldException -> return None
253 | }
254 |
255 | member this.Count ( path: string ) : Task = task {
256 | let! v = this.AllString path
257 | return v.Length
258 | }
259 |
260 | member this.Raw with get () : Task = task {
261 | let! isPresent = this.IsPresent ()
262 |
263 | if isPresent then
264 | return json.Value
265 | else
266 | return raise ( MissingRequiredJsonException () )
267 | }
268 |
269 | member this.IsPresent () = task {
270 | if this.IsJsonContentType then
271 | try
272 | let! _ = getJson ()
273 | return true
274 | with
275 | | :? MissingRequiredJsonException -> return false
276 | else
277 | return false
278 | }
279 | member val IsJsonContentType = not notJsonContentType
280 |
281 | type Body ( req: HttpRequest, jsonSettingsProvider: Lazy ) =
282 | member val Form = FormEncodedBody req
283 | member val Json = JsonEncodedBody ( req, lazy jsonSettingsProvider.Value.Settings )
284 | member val Raw = req.Body
285 | member val RawPipe = req.BodyReader
286 |
--------------------------------------------------------------------------------
/samples/AdvancedServer/Program.fs:
--------------------------------------------------------------------------------
1 | namespace AdvancedServer
2 |
3 | open System
4 | open System.IO
5 |
6 | open Microsoft.AspNetCore.Hosting
7 | open Microsoft.AspNetCore.Builder
8 |
9 | open Microsoft.Extensions.DependencyInjection
10 | open Microsoft.Extensions.Hosting
11 |
12 | open WebFrame
13 | open WebFrame.Http
14 | open WebFrame.RouteTypes
15 | open WebFrame.SystemConfig
16 |
17 | open type WebFrame.Endpoints.Helpers
18 |
19 | type SampleRequestBody = {
20 | Value: decimal
21 | Name: string
22 | }
23 |
24 | // Sample Exception
25 | type CoffeeException () =
26 | inherit Exception "I am a teapot!"
27 |
28 | // Sample service to inject
29 | type IMyService =
30 | abstract member Print : string -> unit
31 |
32 | type MyService () =
33 | interface IMyService with
34 | member _.Print text = printfn $"Text: {text}"
35 |
36 | module MyService =
37 | let configureService: ServiceSetup = fun env config serv ->
38 | serv.AddScoped ()
39 |
40 | module MyApp =
41 | let configureApp: AppSetup = fun env config app ->
42 | if env.IsDevelopment () then
43 | app
44 | else
45 | app.UseExceptionHandler "/error"
46 |
47 |
48 | // These examples will try showing as many available helpers as possible
49 | // However, please refer to the docs for more information
50 | module Program =
51 | // The first item is always the full path to the executable
52 | let args = Environment.GetCommandLineArgs () |> Array.tail
53 |
54 | let app = App args
55 |
56 | // This is a host logger and it is configured in the system defaults
57 | // It is created even before the app is built and running
58 | // If the app raises an exception during the setup,
59 | // then it may terminate before the message is displayed in the console
60 | app.Log.Information "The app has been created"
61 |
62 | // Setting up in-memory overrides for config values
63 | app.Config.[ "MY_OPTION" ] <- "Brave New World!"
64 | app.Config.[ "COUNT" ] <- "12"
65 |
66 | // Returning 418 whenever a "CoffeeException" is raised within this module ("App" only in this case)
67 | app.Errors <- Error.codeFor 418
68 |
69 | // Registering a custom service before registering any services provided by the WebFrame
70 | app.Services.BeforeServices <- MyService.configureService
71 |
72 | // Adding an exception redirection when not in development
73 |
74 | // This is the earliest slot available for the app configuration
75 | // It happens before anything else is configured
76 | app.Services.BeforeApp <- MyApp.configureApp
77 |
78 | // Web Page route
79 | app.Get "/" <- page "Test.html" // It should end .html or Server Side exception would be thrown
80 | // Text response helper
81 | app.Get "/hello" <- always ( TextResponse "world" )
82 | // Few different ways to send a file over
83 | app.Get "/file" <- file "Text.txt" // You can also add a required content-type
84 | app.Get "/file2" <- fun _ -> FileResponse "Text.txt"
85 | app.Get "/file3" <- fun serv -> serv.File "Text.txt" // You can also add a required content-type
86 |
87 | // Accessing a custom service
88 | app.Get "/service" <- fun serv ->
89 | let s = serv.Services.Required ()
90 |
91 | s.Print "Hello"
92 |
93 | serv.EndResponse ()
94 |
95 | app.Get "/fail" <- fun _ ->
96 | failwith "I have failed you."
97 |
98 | // Accessing Configuration
99 | app.Get "/my" <- fun serv ->
100 | // Optional with default
101 | let myOption = serv.Config.Get "MY_OPTION" ""
102 | // Required property that can be parsed into int
103 | let count = serv.Config.Required "COUNT"
104 |
105 | // Returns a string
106 | serv.EndResponse $"[{count}] {myOption}"
107 |
108 | // Always return an empty page with a 404 status code
109 | app.Get "/error" <- fun serv ->
110 | serv.StatusCode <- 404
111 | // Let's log the error too
112 | serv.Log.Warning "A page was not found"
113 | EndResponse
114 |
115 | // EndResponse method returns the HttpWorkload type
116 | // So you do not have to import the type in this case
117 | app.Get "/coffee" <- fun serv ->
118 | CoffeeException () |> raise
119 | serv.EndResponse ()
120 |
121 | // Requesting ASP.NET Core services
122 | // and returning objects (json by default)
123 | app.Get "/env" <- fun serv ->
124 | let env = serv.Services.Required ()
125 |
126 | serv.EndResponse env
127 |
128 | // Showing route and query parameters
129 | // For the route pattern syntax please check ASP.NET Core docs
130 | app.Post "/new/{item:guid}/{groupId:int?}/{**slug}" <- fun serv ->
131 | // Route Values are always singular
132 | let slug = serv.Route.Get "slug" ""
133 | let item = serv.Route.Required "item"
134 | let groupId = serv.Route.Optional "groupId"
135 |
136 | // Query Values are normally returned as a list
137 | // All values found must match the specified type
138 |
139 | // Only Get method tries to return the first item
140 | // It returns the first item found in the list
141 | let order = serv.Query.Get "order" "desc"
142 | // Required will return all the items found in a list
143 | // But they all have to match the specified type
144 | // Otherwise it fails
145 | let q = serv.Query.Required "q"
146 | let nextPosition = serv.Query.Optional "next"
147 | // Will try find all the query parameters names "custom"
148 | // and returns them as a list of strings
149 | // Returns an empty list if nothing is found
150 | let allCustomQ = serv.Query.All "custom"
151 |
152 | // A json response with an anonymous record
153 | serv.EndResponse
154 | {|
155 | Item = item
156 | GroupId = groupId
157 | Slug = slug
158 | Ordering = order
159 | Q = q
160 | Next = nextPosition
161 | Custom = allCustomQ
162 | |}
163 |
164 | // Accessing headers and cookies
165 | app.Get "/name" <- fun serv ->
166 | // Headers and Cookies follow the same principal as the Route and Query Parameters
167 | // You can request Required, Optional and so on
168 | // Headers are Cookies are string values
169 | // In addition, Headers are represented as a list
170 |
171 | // Requesting Optional Header from the request with a default value
172 | let specialHeader = serv.Headers.Get "Custom" "None"
173 | // Reading Optional Cookie with a default value
174 | let randomCookie = serv.Cookies.Get "Random" "0"
175 |
176 | // Setting up a custom header on the response
177 | // Header methods that write to the response: Set, Append, Delete
178 | serv.Headers.Set "Custom" [ "Random" ]
179 |
180 | let rnd = Random ()
181 | let randomNumber = rnd.Next ( 1, 10 )
182 |
183 | if randomNumber < 8 then
184 | // Setting up a custom Cookie on the response
185 | serv.Cookies.Set "Random" $"{randomNumber}"
186 | else
187 | // Asking for the cookie to be marked as expired
188 | serv.Cookies.Delete "Random"
189 |
190 | // In addition, you can pass ASP.NET Core CookieOptions class to customise them even further
191 | // Please use SetWithOptions and DeleteWithOptions methods for that purpose
192 |
193 | serv.EndResponse $"Custom: {specialHeader} [{randomCookie}]"
194 |
195 | // Another example of a json response
196 | app.Put "/new/{name}" <- fun serv ->
197 | // Another way to declare expected type
198 | let name: string = serv.Route.Required "name"
199 |
200 | // Presence of Required Form Fields would imply that
201 | // the form is sent correctly
202 | let number: int list = serv.Body.Form.Required "number"
203 | // The Form works just like the queries
204 | let zipCode = serv.Body.Form.Get "zip" ""
205 |
206 | // You can check explicitly if the form is present
207 | let present = serv.Body.Form.IsPresent
208 |
209 | let number = number |> List.tryHead |> Option.defaultValue 0
210 |
211 | let response =
212 | {|
213 | Name = name
214 | Number = $"+44{number}"
215 | ZipCode = zipCode
216 | FormWasPresent = present
217 | |}
218 |
219 | JsonResponse response
220 |
221 | // Async Workflows
222 |
223 | // Sometimes the request handler needs to work with Tasks
224 | // Then you can use a separate set of helper methods
225 | app.PostTask "/new" <- fun serv -> task {
226 | // Asynchronously (Task) read the body
227 | // It must be present and of the specified type
228 | // You can use Optional instead if you want to do something special
229 | // when the body is not an anticipated json
230 | let! body = serv.Body.Json.Exact ()
231 |
232 | // A shortcut for accessing Content-Type header
233 | let contentType = serv.ContentType
234 |
235 | return JsonResponse {| Body = body; ContentType = contentType |}
236 | }
237 |
238 | // Another example of reading json
239 | app.PostTask "/new-item" <- fun serv -> task {
240 | // The type can be inlined and anonymous
241 | // Furthermore, it will discard additional fields if present
242 | let! body = serv.Body.Json.Exact<{| Name: string |}> ()
243 |
244 | return JsonResponse {| Body = body |}
245 | }
246 |
247 | // Reading a raw body as a stream
248 | app.PostTask "/new-item2" <- fun serv -> task {
249 | let bodyStream = serv.Body.Raw
250 |
251 | use reader = new StreamReader ( bodyStream )
252 |
253 | let! body = reader.ReadToEndAsync ()
254 |
255 | return serv.EndResponse {| Body = body |}
256 | }
257 |
258 | // Alternative method of setting up routes
259 | // Accepts Tasks only
260 | app.[ Delete "/" ] <- fun serv -> task {
261 | // You can access the entire asp.net core context
262 | // For example, here is the connection ip address
263 | let ip = serv.Context.Connection.RemoteIpAddress
264 |
265 | printfn $"IP: {ip}"
266 |
267 | // Terminates the response
268 | // Useful when you are manually preparing a response
269 | return EndResponse
270 | }
271 |
272 | app.Run ()
273 |
--------------------------------------------------------------------------------
/src/WebFrame/Library.fs:
--------------------------------------------------------------------------------
1 | namespace WebFrame
2 |
3 | open System
4 | open System.Collections.Generic
5 |
6 | open Microsoft.AspNetCore.Http
7 | open Microsoft.Extensions.Hosting
8 |
9 | open WebFrame.Configuration
10 | open WebFrame.Logging
11 | open WebFrame.Exceptions
12 | open WebFrame.Http
13 | open WebFrame.RouteTypes
14 | open WebFrame.ServicesParts
15 | open WebFrame.Services
16 | open WebFrame.SystemConfig
17 |
18 | type Hooks<'T> ( app: 'T ) =
19 | let mutable onStartHooks: ( 'T -> unit ) list = []
20 | let mutable onStopHooks: ( 'T -> unit ) list = []
21 |
22 | member this.AddOnStartHook ( hook: 'T -> unit ) = onStartHooks <- hook :: onStartHooks
23 | member _.AddOnStopHook ( hook: 'T -> unit ) = onStopHooks <- hook :: onStopHooks
24 |
25 | member _.ClearOnStartHooks () = onStartHooks <- []
26 | member _.ClearOnStopHooks () = onStopHooks <- []
27 |
28 | member internal this.RunOnStartHooks () = onStartHooks |> List.iter ( fun i -> i app )
29 | member internal _.RunOnStopHooks () = onStopHooks |> List.iter ( fun i -> i app )
30 |
31 | type AppModule ( prefix: string ) =
32 | let routes = Routes ()
33 | let modules = Dictionary ()
34 | let errorHandlers = List ()
35 |
36 | let addRoute ( route: RoutePattern ) ( handler: TaskServicedHandler ) =
37 | if routes.ContainsKey route then
38 | raise ( DuplicateRouteException ( route.ToString () ) )
39 |
40 | let routeDef =
41 | handler
42 | |> TaskServicedHandler.toTaskHttpHandler
43 | |> RouteDef.createWithHandler route
44 |
45 | routes.[ route ] <- routeDef
46 |
47 | let addModule name ( m: #AppModule ) =
48 | if modules.ContainsKey name then
49 | raise ( DuplicateModuleException name )
50 |
51 | modules.[ name ] <- m
52 |
53 | let asTask ( h: ServicedHandler ) : TaskServicedHandler =
54 | fun ( s: RequestServices ) -> task {
55 | return h s
56 | }
57 |
58 | // Preprocess each route
59 | let preprocessRoute ( r: RouteDef ) =
60 | r |> RouteDef.prefixWith prefix |> RouteDef.onErrors ( List.ofSeq errorHandlers )
61 |
62 | let updateModuleRoute ( moduleName: string ) ( r: KeyValuePair ) =
63 | r.Value |> RouteDef.name $"{moduleName}.{r.Value.Name}"
64 |
65 | let collectModuleRoutes ( i: KeyValuePair ) =
66 | i.Value.CollectRoutes ()
67 | |> Seq.map ( updateModuleRoute i.Key )
68 |
69 | let getLocalRoutes () = routes |> Seq.map ( fun i -> i.Value )
70 | let getInnerRoutes () = modules |> Seq.collect collectModuleRoutes
71 | let preprocessRoutes ( r: RouteDef seq ) = r |> Seq.map preprocessRoute
72 |
73 | member this.Connect
74 | with set ( index: string ) ( value: ServicedHandler ) =
75 | let pattern = RoutePattern.create index [ CONNECT ]
76 | value
77 | |> asTask
78 | |> addRoute pattern
79 | member this.Delete
80 | with set ( index: string ) ( value: ServicedHandler ) =
81 | let pattern = RoutePattern.create index [ DELETE ]
82 | value
83 | |> asTask
84 | |> addRoute pattern
85 | member this.Get
86 | with set ( index: string ) ( value: ServicedHandler ) =
87 | let pattern = RoutePattern.create index [ GET ]
88 | value
89 | |> asTask
90 | |> addRoute pattern
91 | member this.Head
92 | with set ( index: string ) ( value: ServicedHandler ) =
93 | let pattern = RoutePattern.create index [ HEAD ]
94 | value
95 | |> asTask
96 | |> addRoute pattern
97 | member this.Options
98 | with set ( index: string ) ( value: ServicedHandler ) =
99 | let pattern = RoutePattern.create index [ OPTIONS ]
100 | value
101 | |> asTask
102 | |> addRoute pattern
103 | member this.Patch
104 | with set ( index: string ) ( value: ServicedHandler ) =
105 | let pattern = RoutePattern.create index [ PATCH ]
106 | value
107 | |> asTask
108 | |> addRoute pattern
109 | member this.Post
110 | with set ( index: string ) ( value: ServicedHandler ) =
111 | let pattern = RoutePattern.create index [ POST ]
112 | value
113 | |> asTask
114 | |> addRoute pattern
115 | member this.Put
116 | with set ( index: string ) ( value: ServicedHandler ) =
117 | let pattern = RoutePattern.create index [ PUT ]
118 | value
119 | |> asTask
120 | |> addRoute pattern
121 | member this.Trace
122 | with set ( index: string ) ( value: ServicedHandler ) =
123 | let pattern = RoutePattern.create index [ TRACE ]
124 | value
125 | |> asTask
126 | |> addRoute pattern
127 | member this.Any
128 | with set ( index: string ) ( value: ServicedHandler ) =
129 | let pattern = RoutePattern.create index HttpMethod.Any
130 | value
131 | |> asTask
132 | |> addRoute pattern
133 |
134 | member this.ConnectTask
135 | with set ( index: string ) ( value: TaskServicedHandler ) = value |> addRoute ( RoutePattern.create index [ CONNECT ] )
136 | member this.DeleteTask
137 | with set ( index: string ) ( value: TaskServicedHandler ) = value |> addRoute ( RoutePattern.create index [ DELETE ] )
138 | member this.GetTask
139 | with set ( index: string ) ( value: TaskServicedHandler ) = value |> addRoute ( RoutePattern.create index [ GET ] )
140 | member this.HeadTask
141 | with set ( index: string ) ( value: TaskServicedHandler ) = value |> addRoute ( RoutePattern.create index [ HEAD ] )
142 | member this.OptionsTask
143 | with set ( index: string ) ( value: TaskServicedHandler ) = value |> addRoute ( RoutePattern.create index [ OPTIONS ] )
144 | member this.PatchTask
145 | with set ( index: string ) ( value: TaskServicedHandler ) = value |> addRoute ( RoutePattern.create index [ PATCH ] )
146 | member this.PostTask
147 | with set ( index: string ) ( value: TaskServicedHandler ) = value |> addRoute ( RoutePattern.create index [ POST ] )
148 | member this.PutTask
149 | with set ( index: string ) ( value: TaskServicedHandler ) = value |> addRoute ( RoutePattern.create index [ PUT ] )
150 | member this.TraceTask
151 | with set ( index: string ) ( value: TaskServicedHandler ) = value |> addRoute ( RoutePattern.create index [ TRACE ] )
152 | member this.AnyTask
153 | with set ( index: string ) ( value: TaskServicedHandler ) = value |> addRoute ( RoutePattern.create index HttpMethod.Any )
154 |
155 | member _.Module
156 | with set ( name: string ) ( value: AppModule ) = value |> addModule name
157 | and get ( name: string ) = modules.[ name ]
158 |
159 | member _.CollectRoutes () : Routes =
160 | let result = Routes ()
161 |
162 | let addRoute ( route: RouteDef ) =
163 | if result.ContainsKey route.Pattern then
164 | raise ( DuplicateRouteException ( route.Pattern.ToString () ) )
165 |
166 | result.[ route.Pattern ] <- route
167 |
168 | getLocalRoutes ()
169 | |> Seq.append ( getInnerRoutes () )
170 | |> preprocessRoutes
171 | |> Seq.iter addRoute
172 |
173 | result
174 | member this.Errors with set h = h |> errorHandlers.Add
175 | member this.Route with set ( r: RouteDef ) =
176 | if routes.ContainsKey r.Pattern then
177 | raise ( DuplicateRouteException ( r.Pattern.ToString () ) )
178 |
179 | routes.[ r.Pattern ] <- r
180 |
181 | type App ( defaultConfig: SystemDefaults ) as app =
182 | inherit AppModule ""
183 | let args = defaultConfig.Args
184 | let mutable host = None
185 |
186 | new () = App SystemDefaults.standard
187 | new ( args: string [] ) = App ( SystemDefaults.defaultWithArgs args )
188 |
189 | member val Services = SystemSetup defaultConfig
190 | member val Config = ConfigOverrides defaultConfig
191 | member val Hooks = Hooks app
192 | member val Defaults = defaultConfig
193 |
194 | member private this.GetHostBuilder ( ?testServer: bool ) =
195 | let testServer = defaultArg testServer false
196 |
197 | this.Services.Routes <- this.CollectRoutes ()
198 | this.Services.Config <- this.Config
199 |
200 | if testServer then
201 | this.Services.CreateTestBuilder args
202 | else
203 | this.Services.CreateHostBuilder args
204 |
205 | member private this.BuildHost ( builder: IHostBuilder ) : IHost =
206 | let h = builder.Build ()
207 | host <- Some h
208 | h
209 |
210 | member private this.BuildTest () : IHost =
211 | this.GetHostBuilder true // Pass true to enable the test server
212 | |> this.BuildHost
213 |
214 | member this.GetServiceProvider () : GenericServiceProvider =
215 | let h: IHost =
216 | match host with
217 | | Some h -> h
218 | | None -> raise ( HostNotReadyException () )
219 |
220 | h.Services |> GenericServiceProvider
221 |
222 | member this.Build () : IHost =
223 | this.GetHostBuilder ()
224 | |> this.BuildHost
225 |
226 | member this.Run () =
227 | let host = host |> Option.defaultValue ( this.Build () )
228 | try
229 | this.Hooks.RunOnStartHooks ()
230 | host.Run ()
231 | finally
232 | this.Hooks.RunOnStopHooks ()
233 |
234 | member this.Run ( urls: string list ) =
235 | this.Config.[ "urls" ] <- urls |> String.concat ";"
236 | let host = this.Build ()
237 | try
238 | this.Hooks.RunOnStartHooks ()
239 | host.Run ()
240 | finally
241 | this.Hooks.RunOnStopHooks ()
242 |
243 | member this.TestServer () =
244 | let host = this.BuildTest ()
245 | task {
246 | try
247 | this.Hooks.RunOnStartHooks ()
248 | do! host.StartAsync ()
249 | return host
250 | finally
251 | this.Hooks.RunOnStopHooks ()
252 | }
253 |
254 | member val Log =
255 | let f = defaultConfig.LoggerHostFactory
256 | let name = defaultConfig |> SystemDefaults.getGlobalLoggerName
257 | Logger ( lazy f, name )
258 |
259 | module Error =
260 | let onTask<'T when 'T :> exn> ( e: ServicedTaskErrorHandler<'T> ) : TaskErrorHandler =
261 | fun ( configs: SystemDefaults ) ( ex: Exception ) ( c: HttpContext ) ->
262 | task {
263 | match ex with
264 | | :? 'T as ex ->
265 | let! r = e ex ( RequestServices ( c, configs ) )
266 | return Some r
267 | | _ ->
268 | return None
269 | }
270 | let on<'T when 'T :> exn> ( e: ServicedErrorHandler<'T> ) : TaskErrorHandler =
271 | onTask ( fun ex serv -> task { return e ex serv } )
272 |
273 | let codeFor<'T when 'T :> exn> ( code: int ) : TaskErrorHandler =
274 | fun ( e: 'T ) ( serv: RequestServices ) -> task {
275 | let exFilter = serv.Services.Required ()
276 | let message =
277 | if exFilter.ShowUserException then
278 | let t = e.GetType ()
279 | $"{t.Name}: {e.Message}"
280 | else
281 | "Workflow Error"
282 | serv.StatusCode <- code
283 | return serv.EndResponse message
284 | }
285 | |> onTask
286 |
--------------------------------------------------------------------------------
/WebFrame.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30114.105
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{10075972-9166-4D47-87B1-47DC4567CE57}"
7 | EndProject
8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Minimal", "samples\Minimal\Minimal.fsproj", "{3D7380B8-3BD7-4D58-97AD-731D1463CE54}"
9 | EndProject
10 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "LocalServer", "samples\LocalServer\LocalServer.fsproj", "{4E0C0625-E5E8-4EE7-A7F0-8C4D79DC191C}"
11 | EndProject
12 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Modules", "samples\Modules\Modules.fsproj", "{8D830FBE-7B0C-44C4-8B27-914E8F051D2C}"
13 | EndProject
14 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "TestServer", "samples\TestServer\TestServer.fsproj", "{AB0DE2F3-CB50-4F7C-9EA3-292109341F12}"
15 | EndProject
16 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "AdvancedServer", "samples\AdvancedServer\AdvancedServer.fsproj", "{D2304055-B211-4815-9267-A5B193440530}"
17 | EndProject
18 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "StandardServer", "samples\StandardServer\StandardServer.fsproj", "{B5D8BCB2-8231-44BD-846C-F53DEBE88DBC}"
19 | EndProject
20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{829596DB-A8F4-4DF8-89C8-A372C9B7E88D}"
21 | ProjectSection(SolutionItems) = preProject
22 | README.md = README.md
23 | LICENSE = LICENSE
24 | paket.dependencies = paket.dependencies
25 | EndProjectSection
26 | EndProject
27 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WebFrame", "src\WebFrame\WebFrame.fsproj", "{8457CA84-6CFA-4CDD-8A7D-254E24150956}"
28 | EndProject
29 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F49A36A9-1D60-4CB3-BCE0-7FE4B998DA65}"
30 | EndProject
31 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "WebFrame.Tests", "tests\WebFrame.Tests\WebFrame.Tests.fsproj", "{75131042-ADF7-4065-804A-194095945DCA}"
32 | EndProject
33 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "templatepack", "templates\templatepack.fsproj", "{D93A115F-325B-4433-9EFC-1849311C5574}"
34 | EndProject
35 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{7FC46BB0-9E13-4C1D-8540-AB6DF62285E0}"
36 | ProjectSection(SolutionItems) = preProject
37 | .github\workflows\github-actions.yml = .github\workflows\github-actions.yml
38 | .github\workflows\release.yml = .github\workflows\release.yml
39 | EndProjectSection
40 | EndProject
41 | Global
42 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
43 | Debug|Any CPU = Debug|Any CPU
44 | Debug|x64 = Debug|x64
45 | Debug|x86 = Debug|x86
46 | Release|Any CPU = Release|Any CPU
47 | Release|x64 = Release|x64
48 | Release|x86 = Release|x86
49 | EndGlobalSection
50 | GlobalSection(SolutionProperties) = preSolution
51 | HideSolutionNode = FALSE
52 | EndGlobalSection
53 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
54 | {3D7380B8-3BD7-4D58-97AD-731D1463CE54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
55 | {3D7380B8-3BD7-4D58-97AD-731D1463CE54}.Debug|Any CPU.Build.0 = Debug|Any CPU
56 | {3D7380B8-3BD7-4D58-97AD-731D1463CE54}.Debug|x64.ActiveCfg = Debug|Any CPU
57 | {3D7380B8-3BD7-4D58-97AD-731D1463CE54}.Debug|x64.Build.0 = Debug|Any CPU
58 | {3D7380B8-3BD7-4D58-97AD-731D1463CE54}.Debug|x86.ActiveCfg = Debug|Any CPU
59 | {3D7380B8-3BD7-4D58-97AD-731D1463CE54}.Debug|x86.Build.0 = Debug|Any CPU
60 | {3D7380B8-3BD7-4D58-97AD-731D1463CE54}.Release|Any CPU.ActiveCfg = Release|Any CPU
61 | {3D7380B8-3BD7-4D58-97AD-731D1463CE54}.Release|Any CPU.Build.0 = Release|Any CPU
62 | {3D7380B8-3BD7-4D58-97AD-731D1463CE54}.Release|x64.ActiveCfg = Release|Any CPU
63 | {3D7380B8-3BD7-4D58-97AD-731D1463CE54}.Release|x64.Build.0 = Release|Any CPU
64 | {3D7380B8-3BD7-4D58-97AD-731D1463CE54}.Release|x86.ActiveCfg = Release|Any CPU
65 | {3D7380B8-3BD7-4D58-97AD-731D1463CE54}.Release|x86.Build.0 = Release|Any CPU
66 | {4E0C0625-E5E8-4EE7-A7F0-8C4D79DC191C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
67 | {4E0C0625-E5E8-4EE7-A7F0-8C4D79DC191C}.Debug|Any CPU.Build.0 = Debug|Any CPU
68 | {4E0C0625-E5E8-4EE7-A7F0-8C4D79DC191C}.Debug|x64.ActiveCfg = Debug|Any CPU
69 | {4E0C0625-E5E8-4EE7-A7F0-8C4D79DC191C}.Debug|x64.Build.0 = Debug|Any CPU
70 | {4E0C0625-E5E8-4EE7-A7F0-8C4D79DC191C}.Debug|x86.ActiveCfg = Debug|Any CPU
71 | {4E0C0625-E5E8-4EE7-A7F0-8C4D79DC191C}.Debug|x86.Build.0 = Debug|Any CPU
72 | {4E0C0625-E5E8-4EE7-A7F0-8C4D79DC191C}.Release|Any CPU.ActiveCfg = Release|Any CPU
73 | {4E0C0625-E5E8-4EE7-A7F0-8C4D79DC191C}.Release|Any CPU.Build.0 = Release|Any CPU
74 | {4E0C0625-E5E8-4EE7-A7F0-8C4D79DC191C}.Release|x64.ActiveCfg = Release|Any CPU
75 | {4E0C0625-E5E8-4EE7-A7F0-8C4D79DC191C}.Release|x64.Build.0 = Release|Any CPU
76 | {4E0C0625-E5E8-4EE7-A7F0-8C4D79DC191C}.Release|x86.ActiveCfg = Release|Any CPU
77 | {4E0C0625-E5E8-4EE7-A7F0-8C4D79DC191C}.Release|x86.Build.0 = Release|Any CPU
78 | {8D830FBE-7B0C-44C4-8B27-914E8F051D2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
79 | {8D830FBE-7B0C-44C4-8B27-914E8F051D2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
80 | {8D830FBE-7B0C-44C4-8B27-914E8F051D2C}.Debug|x64.ActiveCfg = Debug|Any CPU
81 | {8D830FBE-7B0C-44C4-8B27-914E8F051D2C}.Debug|x64.Build.0 = Debug|Any CPU
82 | {8D830FBE-7B0C-44C4-8B27-914E8F051D2C}.Debug|x86.ActiveCfg = Debug|Any CPU
83 | {8D830FBE-7B0C-44C4-8B27-914E8F051D2C}.Debug|x86.Build.0 = Debug|Any CPU
84 | {8D830FBE-7B0C-44C4-8B27-914E8F051D2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
85 | {8D830FBE-7B0C-44C4-8B27-914E8F051D2C}.Release|Any CPU.Build.0 = Release|Any CPU
86 | {8D830FBE-7B0C-44C4-8B27-914E8F051D2C}.Release|x64.ActiveCfg = Release|Any CPU
87 | {8D830FBE-7B0C-44C4-8B27-914E8F051D2C}.Release|x64.Build.0 = Release|Any CPU
88 | {8D830FBE-7B0C-44C4-8B27-914E8F051D2C}.Release|x86.ActiveCfg = Release|Any CPU
89 | {8D830FBE-7B0C-44C4-8B27-914E8F051D2C}.Release|x86.Build.0 = Release|Any CPU
90 | {AB0DE2F3-CB50-4F7C-9EA3-292109341F12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
91 | {AB0DE2F3-CB50-4F7C-9EA3-292109341F12}.Debug|Any CPU.Build.0 = Debug|Any CPU
92 | {AB0DE2F3-CB50-4F7C-9EA3-292109341F12}.Debug|x64.ActiveCfg = Debug|Any CPU
93 | {AB0DE2F3-CB50-4F7C-9EA3-292109341F12}.Debug|x64.Build.0 = Debug|Any CPU
94 | {AB0DE2F3-CB50-4F7C-9EA3-292109341F12}.Debug|x86.ActiveCfg = Debug|Any CPU
95 | {AB0DE2F3-CB50-4F7C-9EA3-292109341F12}.Debug|x86.Build.0 = Debug|Any CPU
96 | {AB0DE2F3-CB50-4F7C-9EA3-292109341F12}.Release|Any CPU.ActiveCfg = Release|Any CPU
97 | {AB0DE2F3-CB50-4F7C-9EA3-292109341F12}.Release|Any CPU.Build.0 = Release|Any CPU
98 | {AB0DE2F3-CB50-4F7C-9EA3-292109341F12}.Release|x64.ActiveCfg = Release|Any CPU
99 | {AB0DE2F3-CB50-4F7C-9EA3-292109341F12}.Release|x64.Build.0 = Release|Any CPU
100 | {AB0DE2F3-CB50-4F7C-9EA3-292109341F12}.Release|x86.ActiveCfg = Release|Any CPU
101 | {AB0DE2F3-CB50-4F7C-9EA3-292109341F12}.Release|x86.Build.0 = Release|Any CPU
102 | {D2304055-B211-4815-9267-A5B193440530}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
103 | {D2304055-B211-4815-9267-A5B193440530}.Debug|Any CPU.Build.0 = Debug|Any CPU
104 | {D2304055-B211-4815-9267-A5B193440530}.Debug|x64.ActiveCfg = Debug|Any CPU
105 | {D2304055-B211-4815-9267-A5B193440530}.Debug|x64.Build.0 = Debug|Any CPU
106 | {D2304055-B211-4815-9267-A5B193440530}.Debug|x86.ActiveCfg = Debug|Any CPU
107 | {D2304055-B211-4815-9267-A5B193440530}.Debug|x86.Build.0 = Debug|Any CPU
108 | {D2304055-B211-4815-9267-A5B193440530}.Release|Any CPU.ActiveCfg = Release|Any CPU
109 | {D2304055-B211-4815-9267-A5B193440530}.Release|Any CPU.Build.0 = Release|Any CPU
110 | {D2304055-B211-4815-9267-A5B193440530}.Release|x64.ActiveCfg = Release|Any CPU
111 | {D2304055-B211-4815-9267-A5B193440530}.Release|x64.Build.0 = Release|Any CPU
112 | {D2304055-B211-4815-9267-A5B193440530}.Release|x86.ActiveCfg = Release|Any CPU
113 | {D2304055-B211-4815-9267-A5B193440530}.Release|x86.Build.0 = Release|Any CPU
114 | {B5D8BCB2-8231-44BD-846C-F53DEBE88DBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
115 | {B5D8BCB2-8231-44BD-846C-F53DEBE88DBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
116 | {B5D8BCB2-8231-44BD-846C-F53DEBE88DBC}.Debug|x64.ActiveCfg = Debug|Any CPU
117 | {B5D8BCB2-8231-44BD-846C-F53DEBE88DBC}.Debug|x64.Build.0 = Debug|Any CPU
118 | {B5D8BCB2-8231-44BD-846C-F53DEBE88DBC}.Debug|x86.ActiveCfg = Debug|Any CPU
119 | {B5D8BCB2-8231-44BD-846C-F53DEBE88DBC}.Debug|x86.Build.0 = Debug|Any CPU
120 | {B5D8BCB2-8231-44BD-846C-F53DEBE88DBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
121 | {B5D8BCB2-8231-44BD-846C-F53DEBE88DBC}.Release|Any CPU.Build.0 = Release|Any CPU
122 | {B5D8BCB2-8231-44BD-846C-F53DEBE88DBC}.Release|x64.ActiveCfg = Release|Any CPU
123 | {B5D8BCB2-8231-44BD-846C-F53DEBE88DBC}.Release|x64.Build.0 = Release|Any CPU
124 | {B5D8BCB2-8231-44BD-846C-F53DEBE88DBC}.Release|x86.ActiveCfg = Release|Any CPU
125 | {B5D8BCB2-8231-44BD-846C-F53DEBE88DBC}.Release|x86.Build.0 = Release|Any CPU
126 | {8457CA84-6CFA-4CDD-8A7D-254E24150956}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
127 | {8457CA84-6CFA-4CDD-8A7D-254E24150956}.Debug|Any CPU.Build.0 = Debug|Any CPU
128 | {8457CA84-6CFA-4CDD-8A7D-254E24150956}.Debug|x64.ActiveCfg = Debug|Any CPU
129 | {8457CA84-6CFA-4CDD-8A7D-254E24150956}.Debug|x64.Build.0 = Debug|Any CPU
130 | {8457CA84-6CFA-4CDD-8A7D-254E24150956}.Debug|x86.ActiveCfg = Debug|Any CPU
131 | {8457CA84-6CFA-4CDD-8A7D-254E24150956}.Debug|x86.Build.0 = Debug|Any CPU
132 | {8457CA84-6CFA-4CDD-8A7D-254E24150956}.Release|Any CPU.ActiveCfg = Release|Any CPU
133 | {8457CA84-6CFA-4CDD-8A7D-254E24150956}.Release|Any CPU.Build.0 = Release|Any CPU
134 | {8457CA84-6CFA-4CDD-8A7D-254E24150956}.Release|x64.ActiveCfg = Release|Any CPU
135 | {8457CA84-6CFA-4CDD-8A7D-254E24150956}.Release|x64.Build.0 = Release|Any CPU
136 | {8457CA84-6CFA-4CDD-8A7D-254E24150956}.Release|x86.ActiveCfg = Release|Any CPU
137 | {8457CA84-6CFA-4CDD-8A7D-254E24150956}.Release|x86.Build.0 = Release|Any CPU
138 | {75131042-ADF7-4065-804A-194095945DCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
139 | {75131042-ADF7-4065-804A-194095945DCA}.Debug|Any CPU.Build.0 = Debug|Any CPU
140 | {75131042-ADF7-4065-804A-194095945DCA}.Debug|x64.ActiveCfg = Debug|Any CPU
141 | {75131042-ADF7-4065-804A-194095945DCA}.Debug|x64.Build.0 = Debug|Any CPU
142 | {75131042-ADF7-4065-804A-194095945DCA}.Debug|x86.ActiveCfg = Debug|Any CPU
143 | {75131042-ADF7-4065-804A-194095945DCA}.Debug|x86.Build.0 = Debug|Any CPU
144 | {75131042-ADF7-4065-804A-194095945DCA}.Release|Any CPU.ActiveCfg = Release|Any CPU
145 | {75131042-ADF7-4065-804A-194095945DCA}.Release|Any CPU.Build.0 = Release|Any CPU
146 | {75131042-ADF7-4065-804A-194095945DCA}.Release|x64.ActiveCfg = Release|Any CPU
147 | {75131042-ADF7-4065-804A-194095945DCA}.Release|x64.Build.0 = Release|Any CPU
148 | {75131042-ADF7-4065-804A-194095945DCA}.Release|x86.ActiveCfg = Release|Any CPU
149 | {75131042-ADF7-4065-804A-194095945DCA}.Release|x86.Build.0 = Release|Any CPU
150 | {D93A115F-325B-4433-9EFC-1849311C5574}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
151 | {D93A115F-325B-4433-9EFC-1849311C5574}.Debug|Any CPU.Build.0 = Debug|Any CPU
152 | {D93A115F-325B-4433-9EFC-1849311C5574}.Debug|x64.ActiveCfg = Debug|Any CPU
153 | {D93A115F-325B-4433-9EFC-1849311C5574}.Debug|x64.Build.0 = Debug|Any CPU
154 | {D93A115F-325B-4433-9EFC-1849311C5574}.Debug|x86.ActiveCfg = Debug|Any CPU
155 | {D93A115F-325B-4433-9EFC-1849311C5574}.Debug|x86.Build.0 = Debug|Any CPU
156 | {D93A115F-325B-4433-9EFC-1849311C5574}.Release|Any CPU.ActiveCfg = Release|Any CPU
157 | {D93A115F-325B-4433-9EFC-1849311C5574}.Release|Any CPU.Build.0 = Release|Any CPU
158 | {D93A115F-325B-4433-9EFC-1849311C5574}.Release|x64.ActiveCfg = Release|Any CPU
159 | {D93A115F-325B-4433-9EFC-1849311C5574}.Release|x64.Build.0 = Release|Any CPU
160 | {D93A115F-325B-4433-9EFC-1849311C5574}.Release|x86.ActiveCfg = Release|Any CPU
161 | {D93A115F-325B-4433-9EFC-1849311C5574}.Release|x86.Build.0 = Release|Any CPU
162 | EndGlobalSection
163 | GlobalSection(NestedProjects) = preSolution
164 | {3D7380B8-3BD7-4D58-97AD-731D1463CE54} = {10075972-9166-4D47-87B1-47DC4567CE57}
165 | {4E0C0625-E5E8-4EE7-A7F0-8C4D79DC191C} = {10075972-9166-4D47-87B1-47DC4567CE57}
166 | {8D830FBE-7B0C-44C4-8B27-914E8F051D2C} = {10075972-9166-4D47-87B1-47DC4567CE57}
167 | {AB0DE2F3-CB50-4F7C-9EA3-292109341F12} = {10075972-9166-4D47-87B1-47DC4567CE57}
168 | {D2304055-B211-4815-9267-A5B193440530} = {10075972-9166-4D47-87B1-47DC4567CE57}
169 | {B5D8BCB2-8231-44BD-846C-F53DEBE88DBC} = {10075972-9166-4D47-87B1-47DC4567CE57}
170 | {8457CA84-6CFA-4CDD-8A7D-254E24150956} = {829596DB-A8F4-4DF8-89C8-A372C9B7E88D}
171 | {75131042-ADF7-4065-804A-194095945DCA} = {F49A36A9-1D60-4CB3-BCE0-7FE4B998DA65}
172 | {7FC46BB0-9E13-4C1D-8540-AB6DF62285E0} = {829596DB-A8F4-4DF8-89C8-A372C9B7E88D}
173 | EndGlobalSection
174 | EndGlobal
175 |
--------------------------------------------------------------------------------
/tests/WebFrame.Tests/BasicTests.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.Tests.BasicTests
2 |
3 | open System
4 | open System.Globalization
5 | open System.Net
6 | open System.Net.Http
7 |
8 | open Microsoft.Extensions.Configuration
9 | open NUnit.Framework
10 | open FsUnitTyped
11 |
12 | open Microsoft.AspNetCore.TestHost
13 |
14 | open WebFrame
15 | open WebFrame.Exceptions
16 | open WebFrame.Http
17 | open WebFrame.RouteTypes
18 | open WebFrame.Services
19 | open type Endpoints.Helpers
20 |
21 | open Helpers
22 |
23 | type CoffeeException () = inherit Exception "I am a teapot!"
24 | type NotEnoughCoffeeException () = inherit Exception "We need more coffee!"
25 | type TooMuchCoffeeException () = inherit Exception "We've had enough coffee already!"
26 |
27 | type MyRecord = {
28 | Id: Guid
29 | Name: string
30 | Position: int
31 | }
32 | type AnotherRecord = {
33 | Value: string
34 | Number: int
35 | }
36 | type NestedResponse<'T> = {
37 | Name: string
38 | Data: 'T
39 | }
40 | let app = App ()
41 |
42 | let api = AppModule "/api"
43 | api.Get "/log" <- fun serv ->
44 | serv.Log.Information "Test Log"
45 |
46 | let description = serv.RouteDescription
47 |
48 | serv.Log.Information $"Name: {description.Name}"
49 |
50 | serv.EndResponse ()
51 |
52 | let localization = AppModule "/loc"
53 | localization.Get "/culture" <- fun serv ->
54 | let c = serv.Globalization.RequestCulture
55 | serv.EndResponse c.Name
56 |
57 | app.Log.Information "Hello"
58 |
59 | app.Services.Globalization.AllowedCultures <- [
60 | CultureInfo "en"
61 | CultureInfo "en-GB"
62 | CultureInfo "fr"
63 | CultureInfo "ru-RU"
64 | ]
65 |
66 | app.Config.[ "hello" ] <- "World"
67 |
68 | app.GetTask "/" <- fun _ -> task { return TextResponse "Hello World!" }
69 | app.GetTask "/data" <- alwaysTask ( TextResponse "Data" )
70 | app.Post "/data" <- fun serv ->
71 | let q = serv.Query.Get "q" ""
72 | JsonResponse {| Q = q |}
73 |
74 | app.PostTask "/guid" <- fun serv -> task {
75 | let! testField = serv.Body.Json.Optional "id"
76 | let field = testField |> Option.flatten
77 |
78 | return serv.EndResponse {| Result = field |}
79 | }
80 |
81 | app.Get "/log/{groupId:int?}" <- fun serv ->
82 | serv.Log.Information "Test Log"
83 |
84 | let description = serv.RouteDescription
85 |
86 | serv.Log.Information $"Name: {description.Name}"
87 |
88 | serv.EndResponse ()
89 |
90 | app.Module "api" <- api
91 | app.Module "localization" <- localization
92 |
93 | []
94 | let Setup () =
95 | ()
96 |
97 | []
98 | let ``Verifies that the test server is alive`` () = task {
99 | use! server = app.TestServer ()
100 | use client = server.GetTestClient ()
101 |
102 | let! r = client.GetAsync "/"
103 | let! content = r.Content.ReadAsStringAsync ()
104 |
105 | r.StatusCode |> shouldEqual HttpStatusCode.OK
106 | content |> shouldEqual "Hello World!"
107 |
108 | let! r = client.GetAsync "/unknown"
109 |
110 | r.StatusCode |> shouldEqual HttpStatusCode.NotFound
111 |
112 | let! r = client.GetAsync "/data"
113 | let! content = r.Content.ReadAsStringAsync ()
114 |
115 | r.StatusCode |> shouldEqual HttpStatusCode.OK
116 | content |> shouldEqual "Data"
117 |
118 | let data = Map [
119 | "a", "f"
120 | ]
121 | use requestContent = new FormUrlEncodedContent ( data )
122 | let! r = client.PostAsync ( "/data?q=123", requestContent )
123 | let! content = r.Content.ReadAsStringAsync ()
124 |
125 | content |> shouldEqual """{"Q":"123"}"""
126 |
127 | use requestContent = new FormUrlEncodedContent ( data )
128 | let! r = client.PostAsync ( "/data?n=123", requestContent )
129 | let! content = r.Content.ReadAsStringAsync ()
130 |
131 | content |> shouldEqual """{"Q":""}"""
132 | }
133 |
134 | []
135 | let ``Guid option json test`` () = task {
136 | use! server = app.TestServer ()
137 | use client = server.GetTestClient ()
138 | let data = """
139 | {
140 | "id": {
141 | "Case": "Some",
142 | "Fields": [
143 | "00000000-0000-0000-0000-000000000001"
144 | ]
145 | }
146 | }
147 | """
148 | let expected = """{"Result":{"Case":"Some","Fields":["00000000-0000-0000-0000-000000000001"]}}"""
149 | use c = new StringContent ( data )
150 | c.Headers.ContentType <- Headers.MediaTypeHeaderValue.Parse "application/json"
151 | let! r = client.PostAsync ( "/guid", c )
152 |
153 | let! content = r.Content.ReadAsStringAsync ()
154 |
155 | content |> shouldEqual expected
156 | return ()
157 | }
158 |
159 | []
160 | let ``Logging works as expected`` () = task {
161 | // TODO: actually make this test work
162 | use! server = app.TestServer ()
163 | use client = server.GetTestClient ()
164 |
165 | let! _ = client.GetAsync "/log"
166 | let! _ = client.GetAsync "/api/log"
167 |
168 | return ()
169 | }
170 |
171 | []
172 | let ``Verifying the IServiceProvider getter method logic`` () = task {
173 | let expectedResponse = "Hello World!"
174 |
175 | // Creating a local app instance in order to avoid conflicts with other tests
176 | let app = App ()
177 | app.Get "/" <- always expectedResponse
178 |
179 | let getServiceProvider = app.GetServiceProvider
180 |
181 | fun _ -> getServiceProvider () |> ignore
182 | |> shouldFail
183 |
184 | app.Build () |> ignore
185 | let a1 = getServiceProvider ()
186 | let h1 = a1.GetHashCode ()
187 |
188 | app.Build () |> ignore
189 | let a2 = getServiceProvider ()
190 | let h2 = a2.GetHashCode ()
191 |
192 | h1 |> shouldNotEqual h2
193 |
194 | use! server = app.TestServer ()
195 | use client = server.GetTestClient ()
196 |
197 | let! r = client.GetAsync "/"
198 | let! content = r.Content.ReadAsStringAsync ()
199 |
200 | r.StatusCode |> shouldEqual HttpStatusCode.OK
201 | let ct = r.Content.Headers.ContentType.ToString ()
202 | ct |> shouldEqual "text/plain"
203 | content |> shouldEqual expectedResponse
204 |
205 | let a3 = getServiceProvider ()
206 | let h3 = a3.GetHashCode ()
207 |
208 | h3 |> shouldNotEqual h1
209 | h3 |> shouldNotEqual h2
210 | }
211 |
212 | []
213 | let ``Confirming that the Service Provider returned from App works`` () = task {
214 | use! _server = app.TestServer ()
215 | let serviceProvider = app.GetServiceProvider ()
216 | let confService = serviceProvider.Required ()
217 |
218 | confService.[ "Hello" ] |> shouldEqual "World"
219 | }
220 |
221 | []
222 | let ``Testing basic hooks`` () = task {
223 | let expectedResponse = "Hello World!"
224 | let app = App ()
225 | app.Get "/" <- always expectedResponse
226 |
227 | let mutable onStartHookRan = false
228 | let mutable onStopHookRan = false
229 |
230 | fun _ -> onStartHookRan <- true
231 | |> app.Hooks.AddOnStartHook
232 |
233 | fun _ -> onStopHookRan <- true
234 | |> app.Hooks.AddOnStopHook
235 |
236 | use! server = app.TestServer ()
237 |
238 | do! server.StopAsync ()
239 |
240 | onStartHookRan |> shouldEqual true
241 | onStartHookRan |> shouldEqual true
242 | }
243 |
244 | [Title\nWorld
", IncludePlatform="MacOsX" )>]
245 | [Title\nWorld
", IncludePlatform="Unix, Linux" )>]
246 | [Title\r\nWorld
", IncludePlatform="Win" )>]
247 | let ``Testing basic DotLiquid templating `` ( expectedContent:string ) = task {
248 | use _ = new EnvVar ( "ASPNETCORE_ENVIRONMENT", "Development" )
249 | let app = App ()
250 |
251 | app.Services.ContentRoot <- __SOURCE_DIRECTORY__
252 | app.Get "/" <- fun serv ->
253 | serv.Page "Index.liquid" {| Hello = "World" |}
254 |
255 | app.Get "/about" <- page "About.html"
256 | app.Get "/txt" <- file "Sample.txt"
257 | app.Get "/txt1" <- file ( "Sample.txt", "text/html" )
258 | app.Get "/txt2" <- file "About.html"
259 |
260 | use! server = app.TestServer ()
261 | use client = server.GetTestClient ()
262 |
263 | let! r = client.GetAsync "/"
264 | let! content = r.Content.ReadAsStringAsync ()
265 |
266 | r.StatusCode |> shouldEqual HttpStatusCode.OK
267 | let ct = r.Content.Headers.ContentType.ToString ()
268 | ct |> shouldEqual "text/html"
269 | content |> shouldEqual expectedContent
270 |
271 | let! r = client.GetAsync "/"
272 | let! content = r.Content.ReadAsStringAsync ()
273 |
274 | r.StatusCode |> shouldEqual HttpStatusCode.OK
275 | let ct = r.Content.Headers.ContentType.ToString ()
276 | ct |> shouldEqual "text/html"
277 | content |> shouldEqual expectedContent
278 |
279 | let! r = client.GetAsync "/txt"
280 | let! content = r.Content.ReadAsStringAsync ()
281 |
282 | r.StatusCode |> shouldEqual HttpStatusCode.OK
283 | let ct = r.Content.Headers.ContentType.ToString ()
284 | ct |> shouldEqual "text/plain"
285 | content |> shouldEqual """sample file"""
286 |
287 | let! r = client.GetAsync "/txt1"
288 | let! content = r.Content.ReadAsStringAsync ()
289 |
290 | r.StatusCode |> shouldEqual HttpStatusCode.OK
291 | let ct = r.Content.Headers.ContentType.ToString ()
292 | ct |> shouldEqual "text/html"
293 | content |> shouldEqual """sample file"""
294 |
295 | let! r = client.GetAsync "/txt2"
296 | let! content = r.Content.ReadAsStringAsync ()
297 |
298 | r.StatusCode |> shouldEqual HttpStatusCode.OK
299 | let ct = r.Content.Headers.ContentType.ToString ()
300 | ct |> shouldEqual "text/html"
301 | content |> shouldEqual """About """
302 |
303 | let! r = client.GetAsync "/about"
304 | let! content = r.Content.ReadAsStringAsync ()
305 |
306 | r.StatusCode |> shouldEqual HttpStatusCode.OK
307 | let ct = r.Content.Headers.ContentType.ToString ()
308 | ct |> shouldEqual "text/html"
309 | content |> shouldEqual """About """
310 | }
311 |
312 | []
313 | let ``Checking the nested object response functionality`` () = task {
314 | // TODO: This will not work if the record constructor is private, therefore, it may need a different solution.
315 | let sampleInnerData = [ { Value = "sup"; Number = 3 }; { Value = "another"; Number = 14 } ]
316 | let expectedResult = """{"Name":"Test","Data":[{"Value":"sup","Number":3},{"Value":"another","Number":14}]}"""
317 |
318 | let app = App ()
319 | app.Get "/" <- fun serv ->
320 | let r = {
321 | Name = "Test"
322 | Data = sampleInnerData
323 | }
324 | serv.EndResponse r
325 |
326 | use! server = app.TestServer ()
327 | use client = server.GetTestClient ()
328 |
329 | let! r = client.GetAsync "/"
330 | let! content = r.Content.ReadAsStringAsync ()
331 |
332 | r.StatusCode |> shouldEqual HttpStatusCode.OK
333 | let ct = r.Content.Headers.ContentType.MediaType
334 | ct |> shouldEqual "application/json"
335 | content |> shouldEqual expectedResult
336 | }
337 |
338 | []
339 | let ``Verifying Accept-Language handling`` () = task {
340 | use! server = app.TestServer ()
341 | use client = server.GetTestClient ()
342 |
343 | let! r = client.GetAsync "/loc/culture"
344 | let! content = r.Content.ReadAsStringAsync ()
345 |
346 | r.StatusCode |> shouldEqual HttpStatusCode.OK
347 | content |> shouldEqual CultureInfo.CurrentCulture.Name
348 |
349 | use req = new HttpRequestMessage ( HttpMethod.Get, "/loc/culture" )
350 | req.Headers.AcceptLanguage.ParseAdd "ru-RU"
351 | let! r = client.SendAsync req
352 | let! content = r.Content.ReadAsStringAsync ()
353 |
354 | r.StatusCode |> shouldEqual HttpStatusCode.OK
355 | content |> shouldEqual "ru-RU"
356 |
357 | use req = new HttpRequestMessage ( HttpMethod.Get, "/loc/culture" )
358 | req.Headers.AcceptLanguage.ParseAdd "en, ru-RU; q=0.9, en-GB; q=0.8, fr"
359 | let! r = client.SendAsync req
360 | let! content = r.Content.ReadAsStringAsync ()
361 |
362 | r.StatusCode |> shouldEqual HttpStatusCode.OK
363 | content |> shouldEqual "en"
364 |
365 | use req = new HttpRequestMessage ( HttpMethod.Get, "/loc/culture" )
366 | req.Headers.AcceptLanguage.ParseAdd "ru-RU; q=0.9, en-GB; q=0.8, fr, en"
367 | let! r = client.SendAsync req
368 | let! content = r.Content.ReadAsStringAsync ()
369 |
370 | r.StatusCode |> shouldEqual HttpStatusCode.OK
371 | content |> shouldEqual "fr"
372 |
373 | use req = new HttpRequestMessage ( HttpMethod.Get, "/loc/culture" )
374 | req.Headers.AcceptLanguage.ParseAdd "aa-bb"
375 | let! r = client.SendAsync req
376 | let! content = r.Content.ReadAsStringAsync ()
377 |
378 | r.StatusCode |> shouldEqual HttpStatusCode.OK
379 | content |> shouldEqual CultureInfo.CurrentCulture.Name
380 | }
381 |
382 | []
383 | let ``Checking for custom error handlers`` () = task {
384 | let app = App ()
385 | app.Get "/coffee" <- fun serv ->
386 | raise ( CoffeeException () )
387 | serv.EndResponse ()
388 |
389 | app.Get "/work" <- fun serv ->
390 | raise ( NotEnoughCoffeeException () )
391 | serv.EndResponse ()
392 |
393 | app.Get "/double-shot/coffee" <- fun serv ->
394 | raise ( TooMuchCoffeeException () )
395 | serv.EndResponse ()
396 |
397 | app.Errors <- Error.codeFor 418
398 | app.Errors <- Error.on <| fun ( e: NotEnoughCoffeeException ) serv ->
399 | serv.StatusCode <- 400
400 | serv.EndResponse $"{e.Message}"
401 | app.Errors <- Error.onTask <| fun ( e: TooMuchCoffeeException ) serv -> task {
402 | serv.StatusCode <- 500
403 | return serv.EndResponse $"{e.Message}"
404 | }
405 |
406 | use! server = app.TestServer ()
407 | use client = server.GetTestClient ()
408 |
409 | let! r = client.GetAsync "/coffee"
410 | let! content = r.Content.ReadAsStringAsync ()
411 |
412 | r.StatusCode |> int |> shouldEqual 418
413 | content |> shouldEqual "CoffeeException: I am a teapot!"
414 |
415 | let! r = client.GetAsync "/work"
416 | let! content = r.Content.ReadAsStringAsync ()
417 |
418 | r.StatusCode |> int |> shouldEqual 400
419 | content |> shouldEqual "We need more coffee!"
420 |
421 | let! r = client.GetAsync "/double-shot/coffee"
422 | let! content = r.Content.ReadAsStringAsync ()
423 |
424 | r.StatusCode |> int |> shouldEqual 500
425 | content |> shouldEqual "We've had enough coffee already!"
426 | }
427 |
428 | []
429 | let ``Checking exception filter`` () = task {
430 | use _ = new EnvVar ( "ASPNETCORE_ENVIRONMENT", "Development" )
431 | let app = App ()
432 | app.Services.Exceptions.UserExceptionFilter <-
433 | app.Services.Exceptions.UserExceptionFilter
434 | |> Map.add "Staging" false
435 | app.Services.Exceptions.InputExceptionFilter <-
436 | app.Services.Exceptions.InputExceptionFilter
437 | |> Map.add "Staging" false
438 | app.Services.Exceptions.ServerExceptionFilter <-
439 | app.Services.Exceptions.ServerExceptionFilter
440 | |> Map.add "Staging" true
441 |
442 | app.Get "/coffee" <- fun serv ->
443 | raise ( CoffeeException () )
444 | serv.EndResponse ()
445 |
446 | app.Get "/input" <- fun serv ->
447 | raise ( InputException "My Input Error" )
448 | serv.EndResponse ()
449 |
450 | app.Get "/server" <- fun serv ->
451 | raise ( ServerException "My Server Error" )
452 | serv.EndResponse ()
453 |
454 | app.Errors <- Error.codeFor 418
455 |
456 | app.Build () |> ignore
457 |
458 | // Development Env Tests
459 | use! server = app.TestServer ()
460 | use client = server.GetTestClient ()
461 |
462 | let! r = client.GetAsync "/coffee"
463 | let! content = r.Content.ReadAsStringAsync ()
464 |
465 | r.StatusCode |> int |> shouldEqual 418
466 | content |> shouldEqual "CoffeeException: I am a teapot!"
467 |
468 | let! r = client.GetAsync "/input"
469 | let! content = r.Content.ReadAsStringAsync ()
470 |
471 | r.StatusCode |> int |> shouldEqual 400
472 | content |> shouldEqual "InputException: My Input Error"
473 |
474 | let! r = client.GetAsync "/server"
475 | let! content = r.Content.ReadAsStringAsync ()
476 |
477 | r.StatusCode |> int |> shouldEqual 500
478 | content |> shouldEqual "ServerException: My Server Error"
479 |
480 | // Staging Env Tests
481 | use _ = new EnvVar ( "ASPNETCORE_ENVIRONMENT", "Staging" )
482 | app.Build () |> ignore
483 |
484 | use! server = app.TestServer ()
485 | use client = server.GetTestClient ()
486 |
487 | let! r = client.GetAsync "/coffee"
488 | let! content = r.Content.ReadAsStringAsync ()
489 |
490 | r.StatusCode |> int |> shouldEqual 418
491 | content |> shouldEqual "Workflow Error"
492 |
493 | let! r = client.GetAsync "/input"
494 | let! content = r.Content.ReadAsStringAsync ()
495 |
496 | r.StatusCode |> int |> shouldEqual 400
497 | content |> shouldEqual "Input Validation Error"
498 |
499 | let! r = client.GetAsync "/server"
500 | let! content = r.Content.ReadAsStringAsync ()
501 |
502 | r.StatusCode |> int |> shouldEqual 500
503 | content |> shouldEqual "ServerException: My Server Error"
504 |
505 | // Production Env Tests
506 | use _ = new EnvVar ( "ASPNETCORE_ENVIRONMENT", "Production" )
507 | app.Build () |> ignore
508 |
509 | use! server = app.TestServer ()
510 | use client = server.GetTestClient ()
511 |
512 | let! r = client.GetAsync "/coffee"
513 | let! content = r.Content.ReadAsStringAsync ()
514 |
515 | r.StatusCode |> int |> shouldEqual 418
516 | content |> shouldEqual "CoffeeException: I am a teapot!"
517 |
518 | let! r = client.GetAsync "/input"
519 | let! content = r.Content.ReadAsStringAsync ()
520 |
521 | r.StatusCode |> int |> shouldEqual 400
522 | content |> shouldEqual "InputException: My Input Error"
523 |
524 | let! r = client.GetAsync "/server"
525 | let! content = r.Content.ReadAsStringAsync ()
526 |
527 | r.StatusCode |> int |> shouldEqual 500
528 | content |> shouldEqual "Server Error"
529 | }
530 |
531 | []
532 | let ``Test Direct Route Definition`` () = task {
533 | let expected = "another-bonus"
534 | let app = App ()
535 |
536 | let h =
537 | TaskServicedHandler.toTaskHttpHandler
538 | <| fun serv -> task {
539 | return serv.EndResponse expected
540 | }
541 |
542 | let r =
543 | [ GET; POST ]
544 | |> RoutePattern.create "/bonus"
545 | |> RouteDef.create
546 | |> RouteDef.name "Bonus"
547 | |> RouteDef.handler h
548 |
549 | app.Route <- r
550 |
551 | use! server = app.TestServer ()
552 | use client = server.GetTestClient ()
553 |
554 | let! r = client.GetAsync "/bonus"
555 | let! content = r.Content.ReadAsStringAsync ()
556 |
557 | r.StatusCode |> shouldEqual HttpStatusCode.OK
558 | let ct = r.Content.Headers.ContentType.ToString ()
559 |
560 | ct |> shouldEqual "text/plain"
561 | content |> shouldEqual expected
562 | }
563 |
--------------------------------------------------------------------------------
/src/WebFrame/SystemConfig.fs:
--------------------------------------------------------------------------------
1 | module WebFrame.SystemConfig
2 |
3 | open System
4 | open System.Globalization
5 | open System.IO
6 | open System.Threading.Tasks
7 | open System.Collections.Generic
8 |
9 | open Microsoft.AspNetCore.Builder
10 | open Microsoft.AspNetCore.Hosting
11 | open Microsoft.AspNetCore.Http
12 | open Microsoft.AspNetCore.Routing
13 | open Microsoft.AspNetCore.StaticFiles
14 | open Microsoft.AspNetCore.TestHost
15 |
16 | open Microsoft.Extensions.Configuration
17 | open Microsoft.Extensions.DependencyInjection
18 | open Microsoft.Extensions.Hosting
19 | open Microsoft.Extensions.Logging
20 |
21 | open Newtonsoft.Json
22 |
23 | open WebFrame.Configuration
24 | open WebFrame.Exceptions
25 | open WebFrame.Http
26 | open WebFrame.RouteTypes
27 | open WebFrame.Templating
28 |
29 | type ServiceSetup = IWebHostEnvironment -> IConfiguration -> IServiceCollection -> IServiceCollection
30 | type AppSetup = IWebHostEnvironment -> IConfiguration -> IApplicationBuilder -> IApplicationBuilder
31 | type EndpointSetup = IEndpointRouteBuilder -> IEndpointRouteBuilder
32 |
33 | type TemplateConfiguration = SystemDefaults -> string -> IServiceProvider -> ITemplateRenderer
34 |
35 | type ExceptionSetup ( _defaultConfig: SystemDefaults ) =
36 | member val ShowInputExceptionsByDefault = true with get, set
37 | member val ShowServerExceptionsByDefault = false with get, set
38 | member val ShowUserExceptionsByDefault = true with get, set
39 |
40 | // Environment Name -> true - show / false - hide
41 | member val InputExceptionFilter: Map = Map [] with get, set
42 | member val ServerExceptionFilter: Map =
43 | Map [
44 | Environments.Development, true
45 | ] with get, set
46 |
47 | member val UserExceptionFilter: Map = Map [] with get, set
48 |
49 | member internal this.ShowFullInputExceptionFor ( envName: string ) =
50 | this.InputExceptionFilter
51 | |> Map.tryFind envName
52 | |> Option.defaultValue this.ShowInputExceptionsByDefault
53 | member internal this.ShowFullServerExceptionFor ( envName: string ) =
54 | this.ServerExceptionFilter
55 | |> Map.tryFind envName
56 | |> Option.defaultValue this.ShowServerExceptionsByDefault
57 | member internal this.ShowFullUserExceptionFor ( envName: string ) =
58 | this.UserExceptionFilter
59 | |> Map.tryFind envName
60 | |> Option.defaultValue this.ShowUserExceptionsByDefault
61 |
62 | member internal this.ConfigureServices: ServiceSetup = fun env _ services ->
63 | services.AddSingleton {
64 | new IUserExceptionFilter with
65 | member _.ShowUserException = this.ShowFullUserExceptionFor env.EnvironmentName
66 | }
67 |
68 | type GlobalizationSetup ( _defaultConfig: SystemDefaults ) =
69 | let mutable cultures = set [ CultureInfo.CurrentCulture.Name ]
70 | member val DefaultCulture = CultureInfo.CurrentCulture with get, set
71 | member _.AllowedCultures
72 | with get () = cultures |> Seq.map CultureInfo |> List.ofSeq
73 | and set ( c: CultureInfo list ) = cultures <- c |> Seq.map ( fun i -> i.Name ) |> set
74 |
75 | member internal this.ConfigureServices: ServiceSetup = fun _ _ services ->
76 | services.AddSingleton {
77 | new IGlobalizationConfig with
78 | member _.DefaultCulture = this.DefaultCulture
79 | member _.AllowedCultures = this.AllowedCultures
80 | }
81 |
82 | type JsonSerializationSetup ( _defaultConfig: SystemDefaults ) =
83 | let defaultSettings () =
84 | let s =
85 | JsonSerializerSettings (
86 | ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor )
87 | s
88 |
89 | member this.Default with get () = defaultSettings ()
90 | member val Settings = defaultSettings () with get, set
91 |
92 | type JsonDeserializationService ( settings: JsonSerializerSettings ) =
93 | interface IJsonDeserializationService with
94 | member this.Settings = settings
95 |
96 | type JsonDeserializationSetup ( _defaultConfig: SystemDefaults ) =
97 | let defaultSettings () =
98 | let s =
99 | JsonSerializerSettings (
100 | NullValueHandling = NullValueHandling.Ignore,
101 | MissingMemberHandling = MissingMemberHandling.Error,
102 | ContractResolver = RequireAllPropertiesContractResolver () )
103 | s
104 |
105 | member this.Default with get () = defaultSettings ()
106 | member val Settings = defaultSettings () with get, set
107 | member internal this.ConfigureServices: ServiceSetup = fun _ _ services ->
108 | let settingsProvider = JsonDeserializationService this.Settings
109 | services.AddSingleton settingsProvider
110 |
111 | type JsonSetup ( defaultConfig: SystemDefaults ) =
112 | member val Serialization = JsonSerializationSetup defaultConfig
113 | member val Deserialization = JsonDeserializationSetup defaultConfig
114 |
115 | type TemplatingSetup ( defaultConfig: SystemDefaults ) =
116 | let defaultSetup: TemplateConfiguration = fun defaultConfig root i ->
117 | let loggerFactory = i.GetService ()
118 | let env = i.GetService ()
119 |
120 | DotLiquidTemplateService ( defaultConfig, root, loggerFactory, env )
121 | let mutable userSetup: TemplateConfiguration option = None
122 | let getSetup ( defaultConfig: SystemDefaults ) ( root: string ) =
123 | let setup = userSetup |> Option.defaultValue defaultSetup
124 | setup defaultConfig root
125 |
126 | member val DefaultRenderer = defaultSetup
127 | member val TemplateRoot = "." with get, set
128 | member val Enabled = true with get, set
129 | member _.CustomConfiguration with set ( s: TemplateConfiguration ) = userSetup <- Some s
130 |
131 | member internal this.ConfigureServices: ServiceSetup = fun env _ services ->
132 | if this.Enabled then
133 | let templateRoot = Path.Combine ( env.ContentRootPath, this.TemplateRoot ) |> Path.GetFullPath
134 | let setup = fun i -> getSetup defaultConfig templateRoot i :> obj
135 | let t = typeof
136 | services.AddSingleton ( t, setup )
137 | else
138 | services
139 |
140 | type SimpleRouteDescriptor ( routes: Routes ) =
141 | interface IRouteDescriptorService with
142 | member this.All () = routes |> Seq.map ( fun i -> i.Value ) |> List.ofSeq
143 | member this.TryGet v =
144 | match routes.TryGetValue v with
145 | | true, v -> Some v
146 | | _ -> None
147 |
148 | type InMemoryConfigSetup ( defaultConfig: SystemDefaults ) =
149 | let configStorage = Dictionary ()
150 |
151 | member _.Item
152 | with set ( key: string ) ( value: string ) = configStorage.[ key ] <- value
153 | member val WebFrameSettingsPrefix = defaultConfig.SettingsPrefix
154 | member internal this.SetupWith ( v: ConfigOverrides ) =
155 | for i in v.Raw do this.[ i.Key ] <- i.Value
156 |
157 | member internal _.Builder = fun ( _: HostBuilderContext ) ( config: IConfigurationBuilder ) ->
158 | config.AddInMemoryCollection configStorage |> ignore
159 |
160 | type StaticFilesSetup ( _defaultConfig: SystemDefaults ) =
161 | member val Options: StaticFileOptions option = None with get, set
162 | member val BrowsingOptions: DirectoryBrowserOptions option = None with get, set
163 | member val Route = "" with get, set
164 | member val WebRoot = "wwwroot" with get, set
165 | member val Enabled = false with get, set
166 | member val AllowBrowsing = false with get, set
167 | member internal this.ConfigureApp: AppSetup = fun _env _conf app ->
168 | let app =
169 | if this.Enabled then
170 | match this.Options with
171 | | Some o ->
172 | if this.Route.Length > 0 then o.RequestPath <- this.Route |> PathString
173 | app.UseStaticFiles o
174 | | None ->
175 | if this.Route.Length > 0 then
176 | app.UseStaticFiles this.Route
177 | else
178 | app.UseStaticFiles ()
179 | else
180 | app
181 |
182 | if this.AllowBrowsing then
183 | match this.BrowsingOptions with
184 | | Some o -> app.UseDirectoryBrowser o
185 | | None -> app.UseDirectoryBrowser ()
186 | else
187 | app
188 |
189 | member internal this.ConfigureServices: ServiceSetup = fun _ _ services ->
190 | if this.AllowBrowsing then
191 | services.AddDirectoryBrowser ()
192 | else
193 | services
194 |
195 | type SystemSetup ( defaultConfig: SystemDefaults ) =
196 | let beforeServiceSetup = List ()
197 | let afterServiceSetup = List ()
198 | let beforeAppSetup = List ()
199 | let beforeRoutingSetup = List ()
200 | let beforeEndpointSetup = List ()
201 | let afterEndpointSetup = List ()
202 | let afterAppSetup = List ()
203 | let endpointSetup = List ()
204 | let staticFilesSetup = StaticFilesSetup defaultConfig
205 | let configSetup = InMemoryConfigSetup defaultConfig
206 | let templatingSetup = TemplatingSetup defaultConfig
207 | let jsonSetup = JsonSetup defaultConfig
208 | let globalizationSetup = GlobalizationSetup defaultConfig
209 | let exceptionSetup = ExceptionSetup defaultConfig
210 |
211 | let mutable routes = Routes ()
212 | let mutable contentRoot = ""
213 |
214 | let configureRoute ( env: IWebHostEnvironment ) ( _conf: IConfiguration ) ( route: RouteDef ) ( endpoints: IEndpointRouteBuilder ) =
215 | let prepareDelegate ( eh: TaskErrorHandler list ) ( h: TaskHttpHandler ) =
216 | // Trying to find matching Error Handler
217 | let rec handleExceptions ex ( context: HttpContext ) ( h: TaskErrorHandler list ) = task {
218 | match h with
219 | | [] -> return None
220 | | head::tail ->
221 | match! head defaultConfig ex context with
222 | | Some r -> return Some r
223 | | None -> return! handleExceptions ex context tail
224 | }
225 |
226 | // Calling a handler and trying to catch expected exceptions
227 | let callHandlerWith ( context: HttpContext ) = task {
228 | try
229 | return! h defaultConfig context
230 | with
231 | | ex ->
232 | let eh = eh |> List.rev
233 | let! w = handleExceptions ex context eh
234 | return w |> Option.defaultWith ( fun () -> raise ex )
235 | }
236 |
237 | let handle ( context: HttpContext ) =
238 | task {
239 | try
240 | let! workload = callHandlerWith context
241 | return!
242 | match workload with
243 | | EndResponse -> context.Response.CompleteAsync ()
244 | | TextResponse t ->
245 | if String.IsNullOrEmpty context.Response.ContentType then
246 | context.Response.ContentType <- "text/plain"
247 | context.Response.WriteAsync t
248 | | HtmlResponse t ->
249 | if String.IsNullOrEmpty context.Response.ContentType then
250 | context.Response.ContentType <- "text/html"
251 | context.Response.WriteAsync t
252 | | FileResponse f ->
253 | let path = $"{env.ContentRootPath}/{f}"
254 | if String.IsNullOrEmpty context.Response.ContentType then
255 | let provider = FileExtensionContentTypeProvider ()
256 | let ct =
257 | match provider.TryGetContentType path with
258 | | true, v -> v
259 | | false, _ -> "text/plain"
260 | context.Response.ContentType <- ct
261 | context.Response.SendFileAsync path
262 | | JsonResponse data ->
263 | let output = JsonConvert.SerializeObject ( data, jsonSetup.Serialization.Settings )
264 | if String.IsNullOrEmpty context.Response.ContentType then
265 | context.Response.ContentType <- "application/json; charset=utf-8"
266 | context.Response.WriteAsync output
267 | with
268 | // Catching unhandled exceptions with default handlers
269 | | :? InputException as exn ->
270 | let message =
271 | if exceptionSetup.ShowFullInputExceptionFor env.EnvironmentName then
272 | let t = exn.GetType ()
273 | $"{t.Name}: {exn.Message}"
274 | else
275 | "Input Validation Error"
276 | context.Response.StatusCode <- 400
277 | return! context.Response.WriteAsync message
278 | | :? ServerException as exn ->
279 | let message =
280 | if exceptionSetup.ShowFullServerExceptionFor env.EnvironmentName then
281 | let t = exn.GetType ()
282 | $"{t.Name}: {exn.Message}"
283 | else
284 | "Server Error"
285 | context.Response.StatusCode <- 500
286 | return! context.Response.WriteAsync message
287 | } :> Task
288 |
289 | RequestDelegate handle
290 |
291 | let createEndpoint r =
292 | let handler = r.HttpHandler |> prepareDelegate r.ErrorHandlers
293 | let p = r.Pattern.Path
294 | r.Pattern.Methods
295 | |> List.ofSeq
296 | |> List.sort
297 | |> List.map ( function
298 | | CONNECT ->
299 | endpoints.MapMethods ( p, [ HttpMethods.Connect ], handler )
300 | | DELETE ->
301 | endpoints.MapMethods ( p, [ HttpMethods.Delete ], handler )
302 | | GET ->
303 | endpoints.MapMethods ( p, [ HttpMethods.Get ], handler )
304 | | HEAD ->
305 | endpoints.MapMethods ( p, [ HttpMethods.Head ], handler )
306 | | OPTIONS ->
307 | endpoints.MapMethods ( p, [ HttpMethods.Options ], handler )
308 | | PATCH ->
309 | endpoints.MapMethods ( p, [ HttpMethods.Patch ], handler )
310 | | POST ->
311 | endpoints.MapMethods ( p, [ HttpMethods.Post ], handler )
312 | | PUT ->
313 | endpoints.MapMethods ( p, [ HttpMethods.Put ], handler )
314 | | TRACE ->
315 | endpoints.MapMethods ( p, [ HttpMethods.Trace ], handler )
316 | )
317 |
318 | let setupAuth ( endpoints: IEndpointConventionBuilder list ) =
319 | endpoints
320 | |> List.map (
321 | fun e ->
322 | match route.Auth with
323 | | NoneAuth -> e
324 | | AnonAuth -> e.AllowAnonymous ()
325 | | DefaultAuth -> e.RequireAuthorization ()
326 | | PolicyAuth p -> p |> Array.ofList |> e.RequireAuthorization
327 | | DataAuth d -> d |> Array.ofList |> e.RequireAuthorization
328 | )
329 |
330 | let setupCORS ( endpoints: IEndpointConventionBuilder list ) =
331 | endpoints
332 | |> List.map (
333 | fun e ->
334 | match route.CORS with
335 | | NoneCORS -> e
336 | | PolicyCORS p -> e.RequireCors p
337 | | NewPolicyCORS b -> e.RequireCors b
338 | )
339 |
340 | let setupHosts ( endpoints: IEndpointConventionBuilder list ) =
341 | endpoints
342 | |> List.map (
343 | fun e ->
344 | match route.Host with
345 | | [] -> e
346 | | h -> e.RequireHost ( h |> Array.ofList )
347 | )
348 |
349 | let setupMetadata ( endpoints: IEndpointConventionBuilder list ) =
350 | let descriptor = RouteDescription route :> Object
351 | let metadata = descriptor :: route.Metadata |> Array.ofList
352 |
353 | endpoints |> List.map ( fun e -> e.WithMetadata metadata )
354 |
355 | let setup =
356 | createEndpoint
357 | >> List.map route.PreConfig
358 | >> setupAuth
359 | >> setupCORS
360 | >> setupHosts
361 | >> setupMetadata
362 | >> List.map route.PostConfig
363 | >> ignore
364 |
365 | setup route
366 |
367 | endpoints
368 | let getServiceSetup ( data: List ) : ServiceSetup =
369 | match data.Count with
370 | | 0 -> fun _ _ -> id
371 | | _ -> data |> Seq.reduce ( fun a b -> fun env conf app -> a env conf app |> b env conf )
372 | let getAppSetup ( data: List ) : AppSetup =
373 | match data.Count with
374 | | 0 -> fun _ _ -> id
375 | | _ -> data |> Seq.reduce ( fun a b -> fun env conf app -> a env conf app |> b env conf )
376 | let getEndpointSetup ( data: List ) : EndpointSetup =
377 | match data.Count with
378 | | 0 -> id
379 | | _ -> data |> Seq.reduce ( >> )
380 | let configureServices ( webBuilder: IWebHostBuilder ) =
381 | let c = fun ( ctx: WebHostBuilderContext ) ( serv: IServiceCollection ) ->
382 | let env = ctx.HostingEnvironment
383 | let config = ctx.Configuration
384 |
385 | // Slot for custom services
386 | ( env, config, serv ) |||> getServiceSetup beforeServiceSetup |> ignore
387 |
388 | // Add Route Collection Service
389 | serv.AddSingleton (
390 | fun _ -> SimpleRouteDescriptor routes )
391 | |> ignore
392 |
393 | // Setting up the templating service
394 | ( env, config, serv ) |||> templatingSetup.ConfigureServices |> ignore
395 |
396 | ( env, config, serv ) |||> staticFilesSetup.ConfigureServices |> ignore
397 | ( env, config, serv ) |||> jsonSetup.Deserialization.ConfigureServices |> ignore
398 | ( env, config, serv ) |||> globalizationSetup.ConfigureServices |> ignore
399 | ( env, config, serv ) |||> exceptionSetup.ConfigureServices |> ignore
400 |
401 | // Slot for custom services
402 | ( env, config, serv ) |||> getServiceSetup afterServiceSetup |> ignore
403 |
404 | webBuilder.ConfigureServices ( Action c )
405 | let configureEndpoints ( env: IWebHostEnvironment ) ( conf: IConfiguration ) ( app: IApplicationBuilder ) =
406 | let c = fun ( endpoints: IEndpointRouteBuilder ) ->
407 | let configureRoute = configureRoute env conf
408 |
409 | for route in routes do
410 | let r = route.Value
411 | endpoints |> configureRoute r |> ignore
412 | // Slot for manual endpoints
413 | endpoints |> getEndpointSetup endpointSetup |> ignore
414 | app.UseEndpoints ( Action c ) |> ignore
415 |
416 | let configureApp ( webBuilder: IWebHostBuilder ) =
417 | let c = fun ( ctx: WebHostBuilderContext ) ( app: IApplicationBuilder ) ->
418 | let env = ctx.HostingEnvironment
419 | let config = ctx.Configuration
420 |
421 | // Slot for custom app configurations
422 | ( env, config, app ) |||> getAppSetup beforeAppSetup |> ignore
423 |
424 | if env.IsDevelopment () then
425 | app.UseDeveloperExceptionPage () |> ignore
426 |
427 | // Slot for custom app configurations
428 | ( env, config, app ) |||> getAppSetup beforeRoutingSetup |> ignore
429 |
430 | // Applying static files service configuration if enabled
431 | ( env, config, app ) |||> staticFilesSetup.ConfigureApp |> ignore
432 |
433 | app.UseRouting () |> ignore
434 |
435 | // Slot for custom app configurations
436 | ( env, config, app ) |||> getAppSetup beforeEndpointSetup |> ignore
437 |
438 | ( env, config, app ) |||> configureEndpoints
439 |
440 | // Slot for custom app configurations
441 | ( env, config, app ) |||> getAppSetup afterEndpointSetup |> ignore
442 |
443 | // Slot for custom app configurations
444 | ( env, config, app ) |||> getAppSetup afterAppSetup |> ignore
445 |
446 | webBuilder.Configure ( Action c )
447 |
448 | let configureWebRoot ( webBuilder: IWebHostBuilder ) =
449 | if staticFilesSetup.Enabled then
450 | webBuilder.UseWebRoot staticFilesSetup.WebRoot
451 | else
452 | webBuilder
453 |
454 | let configureContentRoot ( webBuilder: IWebHostBuilder ) =
455 | if contentRoot.Length > 0 then
456 | webBuilder.UseContentRoot contentRoot
457 | else
458 | webBuilder
459 |
460 | let configureConfiguration ( hostBuilder: IHostBuilder ) =
461 | hostBuilder.ConfigureAppConfiguration ( Action configSetup.Builder )
462 |
463 | let configureWebHostDefaults ( c: IWebHostBuilder->unit ) ( hostBuilder: IHostBuilder ) =
464 | hostBuilder.ConfigureWebHostDefaults ( Action c )
465 |
466 | let configureHost ( webBuilder: IWebHostBuilder ) =
467 | webBuilder
468 | |> configureContentRoot
469 | |> configureWebRoot
470 | |> configureServices
471 | |> configureApp
472 | |> ignore
473 |
474 | let configureTestHost =
475 | fun ( webBuilder: IWebHostBuilder ) -> webBuilder.UseTestServer ()
476 | >> configureHost
477 |
478 | let createHostBuilder ( c: IWebHostBuilder->unit ) args =
479 | Host.CreateDefaultBuilder args
480 | |> configureConfiguration
481 | |> configureWebHostDefaults c
482 |
483 | member internal _.CreateHostBuilder args = createHostBuilder configureHost args
484 | member internal _.CreateTestBuilder args = createHostBuilder configureTestHost args
485 | member internal _.Routes with set value = routes <- value
486 | member internal _.Config with set value = configSetup.SetupWith value
487 | member _.BeforeServices with set value = value |> beforeServiceSetup.Add
488 | member _.AfterServices with set value = value |> afterServiceSetup.Add
489 | member _.BeforeApp with set value = value |> beforeAppSetup.Add
490 | member _.BeforeRouting with set value = value |> beforeRoutingSetup.Add
491 | member _.BeforeEndpoints with set value = value |> beforeEndpointSetup.Add
492 | member _.AfterEndpoints with set value = value |> afterEndpointSetup.Add
493 | member _.AfterApp with set value = value |> afterAppSetup.Add
494 | member _.Endpoint with set value = value |> endpointSetup.Add
495 | member val StaticFiles = staticFilesSetup
496 | member val Templating = templatingSetup
497 | member val Json = jsonSetup
498 | member val Globalization = globalizationSetup
499 | member val Exceptions = exceptionSetup
500 | member _.ContentRoot with set value = contentRoot <- value
501 |
--------------------------------------------------------------------------------
/.paket/Paket.Restore.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
8 |
9 | $(MSBuildVersion)
10 | 15.0.0
11 | false
12 | true
13 |
14 | true
15 | $(MSBuildThisFileDirectory)
16 | $(MSBuildThisFileDirectory)..\
17 | $(PaketRootPath)paket-files\paket.restore.cached
18 | $(PaketRootPath)paket.lock
19 | classic
20 | proj
21 | assembly
22 | native
23 | /Library/Frameworks/Mono.framework/Commands/mono
24 | mono
25 |
26 |
27 | $(PaketRootPath)paket.bootstrapper.exe
28 | $(PaketToolsPath)paket.bootstrapper.exe
29 | $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\
30 |
31 | "$(PaketBootStrapperExePath)"
32 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)"
33 |
34 |
35 |
36 |
37 | true
38 | true
39 |
40 |
41 | True
42 |
43 |
44 | False
45 |
46 | $(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/'))
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | $(PaketRootPath)paket
56 | $(PaketToolsPath)paket
57 |
58 |
59 |
60 |
61 |
62 | $(PaketRootPath)paket.exe
63 | $(PaketToolsPath)paket.exe
64 |
65 |
66 |
67 |
68 |
69 | <_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json"))
70 | <_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"'))
71 | <_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | <_PaketCommand>dotnet paket
83 |
84 |
85 |
86 |
87 |
88 | $(PaketToolsPath)paket
89 | $(PaketBootStrapperExeDir)paket
90 |
91 |
92 | paket
93 |
94 |
95 |
96 |
97 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)"))
98 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)"
99 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)"
100 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)"
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | true
122 | $(NoWarn);NU1603;NU1604;NU1605;NU1608
123 | false
124 | true
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)'))
134 |
135 |
136 |
137 |
138 |
139 |
141 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[0].Replace(`"`, ``).Replace(` `, ``))
142 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[1].Replace(`"`, ``).Replace(` `, ``))
143 |
144 |
145 |
146 |
147 | %(PaketRestoreCachedKeyValue.Value)
148 | %(PaketRestoreCachedKeyValue.Value)
149 |
150 |
151 |
152 |
153 | true
154 | false
155 | true
156 |
157 |
158 |
162 |
163 | true
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 | $(PaketIntermediateOutputPath)\$(MSBuildProjectFile).paket.references.cached
183 |
184 | $(MSBuildProjectFullPath).paket.references
185 |
186 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references
187 |
188 | $(MSBuildProjectDirectory)\paket.references
189 |
190 | false
191 | true
192 | true
193 | references-file-or-cache-not-found
194 |
195 |
196 |
197 |
198 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)'))
199 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)'))
200 | references-file
201 | false
202 |
203 |
204 |
205 |
206 | false
207 |
208 |
209 |
210 |
211 | true
212 | target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths)
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | false
224 | true
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length)
236 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0])
237 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1])
238 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4])
239 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5])
240 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[6])
241 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[7])
242 |
243 |
244 | %(PaketReferencesFileLinesInfo.PackageVersion)
245 | All
246 | runtime
247 | $(ExcludeAssets);contentFiles
248 | $(ExcludeAssets);build;buildMultitargeting;buildTransitive
249 | true
250 | true
251 |
252 |
253 |
254 |
255 | $(PaketIntermediateOutputPath)/$(MSBuildProjectFile).paket.clitools
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0])
265 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1])
266 |
267 |
268 | %(PaketCliToolFileLinesInfo.PackageVersion)
269 |
270 |
271 |
272 |
276 |
277 |
278 |
279 |
280 |
281 | false
282 |
283 |
284 |
285 |
286 |
287 | <_NuspecFilesNewLocation Include="$(PaketIntermediateOutputPath)\$(Configuration)\*.nuspec"/>
288 |
289 |
290 |
291 |
292 |
293 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile)
294 | true
295 | false
296 | true
297 | false
298 | true
299 | false
300 | true
301 | false
302 | true
303 | false
304 | true
305 | $(PaketIntermediateOutputPath)\$(Configuration)
306 | $(PaketIntermediateOutputPath)
307 |
308 |
309 |
310 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.$(PackageVersion.Split(`+`)[0]).nuspec"/>
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
370 |
371 |
420 |
421 |
466 |
467 |
511 |
512 |
555 |
556 |
557 |
558 |
--------------------------------------------------------------------------------