├── Fun.AspNetCore.Tests ├── Program.fs ├── Fun.AspNetCore.Tests.fsproj └── EndpointsTests.fs ├── .config └── dotnet-tools.json ├── Fun.AspNetCore ├── CHANGELOG.md ├── Properties │ └── launchSettings.json ├── Types.fs ├── Fun.AspNetCore.fsproj ├── EndpointsDsl.fs ├── EndpointsCEBuilder.fs └── EndpointCEBuilder.fs ├── Fun.AspNetCore.Blazor ├── CHANGELOG.md ├── Properties │ └── launchSettings.json ├── Fun.AspNetCore.Blazor.fsproj └── FunBlazorExtensions.fs ├── Fun.AspNetCore.Demo ├── Properties │ └── launchSettings.json ├── User.fs ├── Fun.AspNetCore.Demo.fsproj ├── Program.fs └── Endpoints.fs ├── .github └── workflows │ ├── Run tests for PR.yml │ └── Build and publish nuget package.yml ├── .editorconfig ├── Fun.AspNetCore.sln ├── README.md └── .gitignore /Fun.AspNetCore.Tests/Program.fs: -------------------------------------------------------------------------------- 1 | module Program = 2 | [] 3 | let main _ = 0 4 | -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "fantomas": { 6 | "version": "5.2.1", 7 | "commands": [ 8 | "fantomas" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Fun.AspNetCore/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.2] - 2023-04-07 4 | 5 | - Add responseCache 6 | 7 | ## [0.1.1] - 2023-02-18 8 | 9 | Add more APIs 10 | 11 | ## [0.1.0] - 2023-02-17 12 | 13 | First release 14 | -------------------------------------------------------------------------------- /Fun.AspNetCore.Blazor/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.2] - 2023-04-07 4 | 5 | - Add enableFunBlazor to process NodeRenderFragment automatically 6 | 7 | ## [0.1.1] - 2023-02-18 8 | 9 | Add more APIs 10 | 11 | ## [0.1.0] - 2023-02-17 12 | 13 | First release 14 | -------------------------------------------------------------------------------- /Fun.AspNetCore/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Fun.AspNetCore": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "https://localhost:64075;http://localhost:64077" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Fun.AspNetCore.Blazor/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Fun.AspNetCore.Blazor": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "https://localhost:64076;http://localhost:64078" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Fun.AspNetCore.Demo/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Fun.AspNetCore.Demo": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "launchUrl": "swagger/index.html", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | }, 10 | "applicationUrl": "https://localhost:51833;http://localhost:51834" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /.github/workflows/Run tests for PR.yml: -------------------------------------------------------------------------------- 1 | name: Run tests for PR 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-20.04 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Setup .NET Core 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 7.0.x 20 | 21 | - name: Use build.fsx 22 | run: dotnet fsi ./build.fsx -- -p test 23 | -------------------------------------------------------------------------------- /Fun.AspNetCore/Types.fs: -------------------------------------------------------------------------------- 1 | namespace Fun.AspNetCore.Internal 2 | 3 | open Microsoft.AspNetCore.Builder 4 | open Microsoft.AspNetCore.Routing 5 | 6 | 7 | type BuildRoute = delegate of group: IEndpointRouteBuilder -> RouteHandlerBuilder 8 | type BuildGroup = delegate of group: IEndpointRouteBuilder -> RouteGroupBuilder 9 | type BuildEndpoint = delegate of route: RouteHandlerBuilder -> RouteHandlerBuilder 10 | type BuildEndpoints = delegate of endpoint: RouteGroupBuilder -> RouteGroupBuilder 11 | -------------------------------------------------------------------------------- /Fun.AspNetCore.Demo/User.fs: -------------------------------------------------------------------------------- 1 | namespace Fun.AspNetCore.Demo 2 | 3 | open System 4 | 5 | 6 | [] 7 | type User = { Id: int; Name: string } 8 | 9 | 10 | module UserApis = 11 | // You can declare the handler in other places instead of inline it in CE 12 | // But, we have to use Func<...> to make fsharp compiled IL will not change the argument name when implicit convert fsharp func to Func delegate 13 | let getUser = Func(fun userId -> { Id = userId; Name = "Foo" }) 14 | -------------------------------------------------------------------------------- /.github/workflows/Build and publish nuget package.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish nuget packages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-20.04 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Setup .NET Core 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 7.0.x 20 | 21 | - name: Use build.fsx 22 | env: 23 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 24 | run: dotnet fsi ./build.fsx -- -p deploy 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{fs,fsx}] 4 | max_line_length=150 5 | fsharp_max_if_then_short_width=80 6 | fsharp_max_if_then_else_short_width=80 7 | fsharp_max_infix_operator_expression=120 8 | fsharp_max_record_width=60 9 | fsharp_max_array_or_list_width=120 10 | fsharp_max_value_binding_width=120 11 | fsharp_max_function_binding_width=120 12 | fsharp_max_dot_get_expression_width=120 13 | fsharp_multiline_block_brackets_on_same_column=true 14 | fsharp_multi_line_lambda_closing_newline=true 15 | fsharp_blank_lines_around_nested_multiline_expressions=false 16 | fsharp_experimental_stroustrup_style=true -------------------------------------------------------------------------------- /Fun.AspNetCore.Demo/Fun.AspNetCore.Demo.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Fun.AspNetCore/Fun.AspNetCore.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | true 6 | library 7 | true 8 | CHANGELOG.md 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Fun.AspNetCore.Blazor/Fun.AspNetCore.Blazor.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | true 6 | library 7 | true 8 | CHANGELOG.md 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Fun.AspNetCore.Demo/Program.fs: -------------------------------------------------------------------------------- 1 | #nowarn "0020" 2 | 3 | open System 4 | open Microsoft.AspNetCore.Http 5 | open Microsoft.AspNetCore.Builder 6 | open Microsoft.Extensions.Hosting 7 | open Microsoft.AspNetCore.Authentication.Cookies 8 | open Microsoft.Extensions.DependencyInjection 9 | open Fun.AspNetCore 10 | open Fun.AspNetCore.Demo 11 | 12 | 13 | let builder = WebApplication.CreateBuilder(Environment.GetCommandLineArgs()) 14 | let services = builder.Services 15 | 16 | services.AddEndpointsApiExplorer() 17 | services.AddSwaggerGen() 18 | services.AddControllersWithViews() 19 | services.AddOutputCache() 20 | services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie() 21 | services.AddAuthorization() 22 | 23 | 24 | let app = builder.Build() 25 | 26 | if app.Environment.IsDevelopment() then 27 | app.UseSwagger() 28 | app.UseSwaggerUI() |> ignore 29 | 30 | app.UseOutputCache() 31 | app.UseAuthentication() 32 | app.UseAuthorization() 33 | 34 | app.MapGroup(Endpoints.apis) 35 | app.MapGroup(Endpoints.view) 36 | 37 | app.MapGroup("normal").MapGet("hi", Func<_>(fun () -> Results.Text "world")) 38 | 39 | app.Run() 40 | -------------------------------------------------------------------------------- /Fun.AspNetCore/EndpointsDsl.fs: -------------------------------------------------------------------------------- 1 | namespace Fun.AspNetCore 2 | 3 | open System.Runtime.CompilerServices 4 | open Microsoft.AspNetCore.Routing 5 | open Fun.AspNetCore.Internal 6 | 7 | 8 | [] 9 | type EndpointsExtensions = 10 | 11 | [] 12 | static member inline MapGroup(this: IEndpointRouteBuilder, [] build: BuildGroup) = build.Invoke(this) 13 | 14 | 15 | [] 16 | module EndpointsDsl = 17 | 18 | /// Get the type from the generic type 'T. 19 | /// The reason for not directly use typeof is because it will make the inline harder and reduce performance. 20 | let inline typedef<'T> () = typeof<'T> 21 | 22 | let inline get pattern = EndpointCEBuilder(EndpointCEBuilder.GetMethods, pattern) 23 | let inline put pattern = EndpointCEBuilder(EndpointCEBuilder.PutMethods, pattern) 24 | let inline post pattern = EndpointCEBuilder(EndpointCEBuilder.PostMethods, pattern) 25 | let inline delete pattern = EndpointCEBuilder(EndpointCEBuilder.DeleteMethods, pattern) 26 | let inline patch pattern = EndpointCEBuilder(EndpointCEBuilder.PatchMethods, pattern) 27 | 28 | let inline endpoints pattern = EndpointsCEBuilder pattern 29 | -------------------------------------------------------------------------------- /Fun.AspNetCore.Tests/Fun.AspNetCore.Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | 6 | false 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | all 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Fun.AspNetCore.Tests/EndpointsTests.fs: -------------------------------------------------------------------------------- 1 | module Fun.AspNetCore.Tests.EndpointsTests 2 | 3 | open System 4 | open System.Text 5 | open System.Net 6 | open System.Net.Http 7 | open Microsoft.AspNetCore.Mvc.Testing 8 | open Xunit 9 | 10 | 11 | type private DemoApplication() = 12 | inherit WebApplicationFactory() 13 | 14 | 15 | let private client = (new DemoApplication()).CreateClient() 16 | 17 | 18 | [] 19 | let ``Simple get api should world`` () = task { 20 | let! actual = client.GetStringAsync("/api/hi") 21 | Assert.Equal("world", actual) 22 | } 23 | 24 | [] 25 | let ``Nested api should work`` () = task { 26 | let! actual = client.GetStringAsync("/api/account/login") 27 | Assert.Equal("logged in", actual) 28 | } 29 | 30 | [] 31 | let ``Auth should work`` () = task { 32 | let! actual = client.GetAsync("/api/user/123") 33 | Assert.Equal(HttpStatusCode.NotFound, actual.StatusCode) 34 | 35 | use content = new StringContent("""{ "Id": 123, "Name": "foo" }""", Encoding.UTF8, "application/json") 36 | let! result = client.PutAsync("/api/user/123", content) 37 | let! actual = result.Content.ReadAsStringAsync() 38 | Assert.Equal("Updated: 123 foo", actual) 39 | } 40 | 41 | [] 42 | let ``Fun Blazor integration should work`` () = task { 43 | let! actual = client.GetStringAsync("/view/blog-list") 44 | Assert.Equal("""""", actual) 45 | 46 | let! actual = client.GetStringAsync("/view/blog/1") 47 | Assert.Equal("""

Blog 1

Please give me feedback if you want.

""", actual) 48 | } 49 | 50 | [] 51 | let ``responseCache should work`` () = task { 52 | let! actual = client.GetAsync("/api/hi2") 53 | Assert.Equal([ $"max-age:{TimeSpan.FromHours(1).TotalSeconds}" ], actual.Headers.GetValues("Cache-Control")) 54 | } 55 | -------------------------------------------------------------------------------- /Fun.AspNetCore.Blazor/FunBlazorExtensions.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Fun.AspNetCore.Extensions 3 | 4 | open System 5 | open System.Threading.Tasks 6 | open Microsoft.AspNetCore.Http 7 | open Microsoft.AspNetCore.Mvc.Rendering 8 | open Microsoft.AspNetCore.Builder 9 | open Microsoft.Extensions.DependencyInjection 10 | open Fun.Blazor 11 | open Fun.AspNetCore.Internal 12 | 13 | 14 | type Results with 15 | 16 | [] 17 | static member inline View(node: NodeRenderFragment) = 18 | { new IResult with 19 | member _.ExecuteAsync(ctx) = ctx.WriteFunDom(node) 20 | } 21 | 22 | 23 | type FunBlazorEndpointFilter(renderMode) = 24 | interface IEndpointFilter with 25 | member _.InvokeAsync(ctx, next) = 26 | task { 27 | match! next.Invoke(ctx) with 28 | | :? NodeRenderFragment as node -> 29 | return 30 | { new IResult with 31 | member _.ExecuteAsync(ctx) = ctx.WriteFunDom(node, renderMode) 32 | } 33 | :> obj 34 | 35 | | x -> return x 36 | } 37 | |> ValueTask 38 | 39 | 40 | type EndpointCEBuilder with 41 | 42 | member inline this.Yield([] node: NodeRenderFragment) = 43 | BuildRoute(fun group -> group.MapMethods(this.Pattern, this.Methods, Func<_>(fun () -> node))) 44 | 45 | 46 | [] 47 | member inline _.enableFunBlazor([] build: BuildEndpoint, ?renderMode) = 48 | BuildEndpoint(fun routeBuilder -> 49 | build 50 | .Invoke(routeBuilder) 51 | .Produces(200, "text/html") 52 | .AddEndpointFilter(FunBlazorEndpointFilter(defaultArg renderMode RenderMode.Static)) 53 | ) 54 | 55 | 56 | type EndpointsCEBuilder with 57 | 58 | [] 59 | member inline _.enableFunBlazor([] build: BuildEndpoints, ?renderMode) = 60 | BuildEndpoints(fun routeGroupBuilder -> 61 | build.Invoke(routeGroupBuilder).AddEndpointFilter(FunBlazorEndpointFilter(defaultArg renderMode RenderMode.Static)) 62 | ) 63 | -------------------------------------------------------------------------------- /Fun.AspNetCore.Demo/Endpoints.fs: -------------------------------------------------------------------------------- 1 | module Fun.AspNetCore.Demo.Endpoints 2 | 3 | open System 4 | open Microsoft.AspNetCore.Http 5 | open Microsoft.AspNetCore.Builder 6 | open Fun.Blazor 7 | open Fun.AspNetCore 8 | 9 | 10 | let apis = 11 | endpoints "api" { 12 | get "hi" { Results.Text "world" } 13 | get "hi1" { 14 | cacheOutput 15 | Results.Text "world" 16 | } 17 | get "hi2" { 18 | responseCache (TimeSpan.FromHours 1) 19 | Results.Text "world" 20 | } 21 | endpoints "user" { 22 | get "{userId}" { 23 | authorization 24 | produces typedef 200 25 | producesProblem 404 26 | handle UserApis.getUser 27 | } 28 | put "{userId}" { 29 | // You can access all apis provided by AspNetCore by use set operation 30 | set (fun route -> route.Accepts("application/json").WithName("foo")) 31 | handle (fun (userId: int) (user: User) -> Results.Text $"Updated: {userId} {user.Name}") 32 | } 33 | } 34 | endpoints "account" { 35 | anonymous 36 | get "login" { handle (fun () -> "logged in") } 37 | } 38 | endpoints "security" { 39 | authorization 40 | tags "high-security" 41 | get "money" { handle (fun () -> "world") } 42 | put "money" { handle (fun () -> "world") } 43 | } 44 | } 45 | 46 | 47 | let view = 48 | endpoints "view" { 49 | // Integrate with Fun.Blazor 50 | enableFunBlazor 51 | 52 | get "time" { 53 | // You can enable for a specific route 54 | enableFunBlazor 55 | cacheOutput (fun b -> b.Expire(TimeSpan.FromSeconds 5)) 56 | div { $"{DateTime.Now}" } 57 | } 58 | get "blog-list" { 59 | produces 200 "text/html" 60 | div { 61 | class' "blog-list my-5" 62 | childContent [ 63 | for i in 1..2 do 64 | a { 65 | href $"/view/blog/{i}" 66 | $"blog {i}" 67 | } 68 | ] 69 | } 70 | } 71 | get "blog/{blogId}" { 72 | produces 200 "text/html" 73 | handle (fun (blogId: int) -> div { 74 | h2 { $"Blog {blogId}" } 75 | p { "Please give me feedback if you want." } 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Fun.AspNetCore.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33213.308 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fun.AspNetCore", "Fun.AspNetCore\Fun.AspNetCore.fsproj", "{DB4126BA-1EC7-49E8-8806-323CE7986725}" 7 | EndProject 8 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fun.AspNetCore.Demo", "Fun.AspNetCore.Demo\Fun.AspNetCore.Demo.fsproj", "{9F472187-28B4-4CB0-9769-8A5187769991}" 9 | EndProject 10 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Fun.AspNetCore.Blazor", "Fun.AspNetCore.Blazor\Fun.AspNetCore.Blazor.fsproj", "{5746704D-9E5D-4046-89BB-CAA38E221608}" 11 | EndProject 12 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Fun.AspNetCore.Tests", "Fun.AspNetCore.Tests\Fun.AspNetCore.Tests.fsproj", "{07010D3A-0A8A-45CD-BF0C-82BC2F7DF499}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {DB4126BA-1EC7-49E8-8806-323CE7986725}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {DB4126BA-1EC7-49E8-8806-323CE7986725}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {DB4126BA-1EC7-49E8-8806-323CE7986725}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {DB4126BA-1EC7-49E8-8806-323CE7986725}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {9F472187-28B4-4CB0-9769-8A5187769991}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {9F472187-28B4-4CB0-9769-8A5187769991}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {9F472187-28B4-4CB0-9769-8A5187769991}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {9F472187-28B4-4CB0-9769-8A5187769991}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {5746704D-9E5D-4046-89BB-CAA38E221608}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {5746704D-9E5D-4046-89BB-CAA38E221608}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {5746704D-9E5D-4046-89BB-CAA38E221608}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {5746704D-9E5D-4046-89BB-CAA38E221608}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {07010D3A-0A8A-45CD-BF0C-82BC2F7DF499}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {07010D3A-0A8A-45CD-BF0C-82BC2F7DF499}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {07010D3A-0A8A-45CD-BF0C-82BC2F7DF499}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {07010D3A-0A8A-45CD-BF0C-82BC2F7DF499}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {F4B9659A-3A72-4F68-A276-6ACA2442A0A6} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fun.AspNetCore [![Nuget](https://img.shields.io/nuget/vpre/Fun.AspNetCore)](https://www.nuget.org/packages/Fun.AspNetCore) 2 | 3 | This is a experimental project for provide a very thin layer on AspNetCore minimal api for fsharp developers who love CE syntax (❤). 4 | 5 | The reason to call it thin layer is because pwoered by the fsharp inline, a lot of overhead will be removed and what it actually compiled is what you may write by using the raw api manually. 6 | 7 | There is a convention for using it: 8 | 9 | **endpoints** is a group of endpoint, it can contain nested **endpoints** or get/put/post/delete/patch endpoints etc. 10 | 11 | ```fsharp 12 | endpoints "api" { 13 | // the settings like authorization, goes first 14 | 15 | // nested endpoints 16 | endpoints "user" { 17 | ... 18 | } 19 | 20 | // single endpoint 21 | get "hi" { ... } 22 | } 23 | ``` 24 | 25 | For a single endpoint it also follow similar pattern 26 | 27 | ```fsharp 28 | get "hi" { 29 | // the settings like authorization, goes first 30 | 31 | // handle should put in the last 32 | handle (fun (v1: T1) (v2: T2) ... -> ...) 33 | // The function argumentS should not be tuples 34 | // You can use function which is defined in other places, but it must be defined as Func<_, _>(fun (v1: T1) (v2: T2) ... -> ...). 35 | // Like: let getUser = Func(fun userId -> { Id = userId; Name = "foo" }) 36 | // The different with csharp minimal api is: you can not add attribute to the argument because of fsharp limitation. 37 | 38 | // You can also yield IResult and NodeRenderFragment(for Fun.Blazor) without use handle, they are special 39 | } 40 | ``` 41 | 42 | 43 | ## Fun.AspNetCore example 44 | 45 | ```fsharp 46 | ... 47 | let builder = WebApplication.CreateBuilder(Environment.GetCommandLineArgs()) 48 | let services = builder.Services 49 | ... 50 | let app = builder.Build() 51 | ... 52 | 53 | app.MapGroup( 54 | endpoints "api" { 55 | get "hi" { Results.Text "world" } 56 | // You can nest endpoints 57 | endpoints "user" { 58 | get "{userId}" { 59 | authorization 60 | produces typedef 200 61 | producesProblem 404 62 | handle UserApis.getUser 63 | } 64 | put "{userId}" { 65 | // You can access all apis provided by AspNetCore by use set operation 66 | set (fun route -> route.Accepts("application/json").WithName("foo")) 67 | handle (fun (userId: int) (user: User) -> Results.Text $"Updated: {userId} {user.Name}") 68 | } 69 | } 70 | endpoints "account" { 71 | anonymous 72 | get "login" { handle (fun () -> "logged in") } 73 | } 74 | endpoints "security" { 75 | authorization 76 | tags "high-security" 77 | get "money" { handle (fun () -> "world") } 78 | put "money" { handle (fun () -> "world") } 79 | } 80 | } 81 | ) 82 | ... 83 | ``` 84 | 85 | ## Fun.AspNetCore.Blazor [![Nuget](https://img.shields.io/nuget/vpre/Fun.AspNetCore.Blazor)](https://www.nuget.org/packages/Fun.AspNetCore.Blazor) example 86 | 87 | ```fsharp 88 | ... 89 | let builder = WebApplication.CreateBuilder(Environment.GetCommandLineArgs()) 90 | let services = builder.Services 91 | ... 92 | services.AddControllersWithViews() // Will register some service for writing dom into response 93 | ... 94 | let app = builder.Build() 95 | ... 96 | 97 | app.MapGroup( 98 | endpoints "view" { 99 | // Integrate with Fun.Blazor 100 | enableFunBlazor 101 | 102 | get "time" { 103 | // You can enable for a specific route 104 | enableFunBlazor 105 | cacheOutput (fun b -> b.Expire(TimeSpan.FromSeconds 5)) 106 | div { $"{DateTime.Now}" } 107 | } 108 | get "blog-list" { 109 | produces 200 "text/html" 110 | div { 111 | class' "blog-list my-5" 112 | childContent [ 113 | for i in 1..2 do 114 | a { 115 | href $"/view/blog/{i}" 116 | $"blog {i}" 117 | } 118 | ] 119 | } 120 | } 121 | get "blog/{blogId}" { 122 | produces 200 "text/html" 123 | handle (fun (blogId: int) -> div { 124 | h2 { $"Blog {blogId}" } 125 | p { "Please give me feedback if you want." } 126 | }) 127 | } 128 | } 129 | ) 130 | ... 131 | ``` 132 | -------------------------------------------------------------------------------- /.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 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd 364 | /reports 365 | -------------------------------------------------------------------------------- /Fun.AspNetCore/EndpointsCEBuilder.fs: -------------------------------------------------------------------------------- 1 | namespace Fun.AspNetCore 2 | 3 | open System 4 | open Microsoft.OpenApi.Models 5 | open Microsoft.AspNetCore.Http 6 | open Microsoft.AspNetCore.Builder 7 | open Microsoft.AspNetCore.Routing 8 | open Microsoft.AspNetCore.Authorization 9 | open Microsoft.AspNetCore.RateLimiting 10 | open Microsoft.AspNetCore.Cors.Infrastructure 11 | open Fun.AspNetCore.Internal 12 | 13 | 14 | type EndpointsCEBuilder(pattern: string) = 15 | 16 | member _.Pattern = pattern 17 | member val UseCustomizedTag = false with get, set 18 | 19 | 20 | member inline this.Run([] build: BuildEndpoints) = 21 | BuildGroup(fun endpoints -> 22 | let group = endpoints.MapGroup(this.Pattern) 23 | if not (String.IsNullOrEmpty this.Pattern) && not this.UseCustomizedTag then 24 | group.WithTags [| this.Pattern |] |> ignore 25 | build.Invoke(group) 26 | ) 27 | 28 | 29 | member inline _.Zero() = BuildEndpoints(fun x -> x) 30 | 31 | member inline _.Yield(_: unit) = BuildEndpoints(fun x -> x) 32 | member inline _.Yield([] build: BuildEndpoints) = BuildEndpoints(fun g -> build.Invoke(g)) 33 | member inline _.Yield([] build: BuildRoute) = 34 | BuildEndpoints(fun g -> 35 | build.Invoke(g) |> ignore 36 | g 37 | ) 38 | member inline _.Yield([] build: BuildGroup) = BuildEndpoints(fun g -> build.Invoke(g)) 39 | member inline _.Delay([] fn: unit -> BuildEndpoints) = BuildEndpoints(fun g -> fn().Invoke(g)) 40 | 41 | 42 | member inline _.Combine([] build1: BuildEndpoints, [] build2: BuildEndpoints) = 43 | BuildEndpoints(fun group -> 44 | build1.Invoke(group) |> ignore 45 | build2.Invoke(group) |> ignore 46 | group 47 | ) 48 | 49 | member inline _.For([] builder: BuildEndpoints, [] fn: unit -> BuildEndpoints) = 50 | BuildEndpoints(fun g -> fn().Invoke(builder.Invoke(g))) 51 | 52 | 53 | /// Give a function to configure the endpoints. 54 | [] 55 | member inline _.set([] build: BuildEndpoints, [] fn: RouteGroupBuilder -> RouteGroupBuilder) = 56 | BuildEndpoints(fun endpoints -> build.Invoke(endpoints) |> fn) 57 | 58 | 59 | /// Registers a filter onto the route handler. 60 | [] 61 | member inline _.filter([] build: BuildEndpoints, filter: IEndpointFilter) = 62 | BuildEndpoints(fun route -> build.Invoke(route).AddEndpointFilter(filter)) 63 | 64 | 65 | /// Adds the Microsoft.AspNetCore.Routing.IExcludeFromDescriptionMetadata to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 66 | [] 67 | member inline _.excludeFromDescription([] build: BuildEndpoints) = 68 | BuildEndpoints(fun route -> build.Invoke(route).ExcludeFromDescription()) 69 | 70 | 71 | /// Adds a CORS policy with the specified name to the endpoint(s). 72 | [] 73 | member inline _.requiredCors([] build: BuildEndpoints, policyName: string) = 74 | BuildEndpoints(fun route -> build.Invoke(route).RequireCors(policyName)) 75 | 76 | /// Adds the specified CORS policy to the endpoint(s). 77 | [] 78 | member inline _.requiredCors([] build: BuildEndpoints, [] config: CorsPolicyBuilder -> CorsPolicyBuilder) = 79 | BuildEndpoints(fun route -> build.Invoke(route).RequireCors(fun x -> config x |> ignore)) 80 | 81 | 82 | /// Requires that endpoints match one of the specified hosts during routing. 83 | [] 84 | member inline _.requireHost([] build: BuildEndpoints, [] hosts: string[]) = 85 | BuildEndpoints(fun route -> build.Invoke(route).RequireHost(hosts)) 86 | 87 | 88 | /// Adds the Microsoft.AspNetCore.Routing.IEndpointNameMetadata to the Metadata collection for all endpoints produced on the target Microsoft.AspNetCore.Builder.IEndpointConventionBuilder given the endpointName. The Microsoft.AspNetCore.Routing.IEndpointNameMetadata on the endpoint is used for link generation and is treated as the operation ID in the given endpoint's OpenAPI specification. 89 | [] 90 | member inline _.name([] build: BuildEndpoints, name: string) = BuildEndpoints(fun group -> build.Invoke(group).WithName(name)) 91 | 92 | /// Sets the Microsoft.AspNetCore.Builder.EndpointBuilder.DisplayName to the provided displayName for all builders created by builder. 93 | [] 94 | member inline _.displayName([] build: BuildEndpoints, name: string) = 95 | BuildEndpoints(fun group -> build.Invoke(group).WithDisplayName(name)) 96 | 97 | /// Sets the Microsoft.AspNetCore.Routing.EndpointGroupNameAttribute for all endpoints produced on the target Microsoft.AspNetCore.Builder.IEndpointConventionBuilder given the endpointGroupName. The Microsoft.AspNetCore.Routing.IEndpointGroupNameMetadata on the endpoint is used to set the endpoint's GroupName in the OpenAPI specification. 98 | [] 99 | member inline _.groupName([] build: BuildEndpoints, name: string) = 100 | BuildEndpoints(fun group -> build.Invoke(group).WithGroupName(name).WithTags()) 101 | 102 | /// Adds Microsoft.AspNetCore.Http.Metadata.IEndpointDescriptionMetadata to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 103 | [] 104 | member inline _.description([] build: BuildEndpoints, description: string) = 105 | BuildEndpoints(fun group -> build.Invoke(group).WithDescription(description)) 106 | 107 | /// Adds Microsoft.AspNetCore.Http.Metadata.IEndpointSummaryMetadata to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 108 | [] 109 | member inline _.summary([] build: BuildEndpoints, summary: string) = 110 | BuildEndpoints(fun group -> build.Invoke(group).WithSummary(summary)) 111 | 112 | /// 113 | /// Adds the to for all endpoints 114 | /// produced by . 115 | /// 116 | /// 117 | /// The OpenAPI specification supports a tags classification to categorize operations 118 | /// into related groups. These tags are typically included in the generated specification 119 | /// and are typically used to group operations by tags in the UI. 120 | /// 121 | [] 122 | member inline this.tags([] build: BuildEndpoints, [] tags: string[]) = 123 | this.UseCustomizedTag <- true 124 | BuildEndpoints(fun group -> build.Invoke(group).WithTags(tags)) 125 | 126 | 127 | /// Adds an OpenAPI annotation to Microsoft.AspNetCore.Http.Endpoint.Metadata associated with the current endpoint. 128 | [] 129 | member inline _.openApi([] build: BuildEndpoints) = BuildEndpoints(fun group -> build.Invoke(group).WithOpenApi()) 130 | 131 | /// Adds an OpenAPI annotation to Microsoft.AspNetCore.Http.Endpoint.Metadata associated with the current endpoint. 132 | [] 133 | member inline _.openApi([] build: BuildEndpoints, [] configOperation: OpenApiOperation -> unit) = 134 | BuildEndpoints(fun group -> 135 | build 136 | .Invoke(group) 137 | .WithOpenApi(fun x -> 138 | configOperation x 139 | x 140 | ) 141 | ) 142 | 143 | 144 | /// Allows anonymous access to the endpoint by adding Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute to the endpoint metadata. This will bypass all authorization checks for the endpoint including the default authorization policy and fallback authorization policy. 145 | [] 146 | member inline _.anonymous([] build: BuildEndpoints) = BuildEndpoints(fun group -> build.Invoke(group).AllowAnonymous()) 147 | 148 | /// Adds the default authorization policy to the endpoint(s). 149 | [] 150 | member inline _.authorization([] build: BuildEndpoints) = BuildEndpoints(fun group -> build.Invoke(group).RequireAuthorization()) 151 | 152 | /// Adds authorization policies with the specified names to the endpoint(s). 153 | [] 154 | member inline _.authorization([] build: BuildEndpoints, policyNames: string[]) = 155 | BuildEndpoints(fun group -> build.Invoke(group).RequireAuthorization(policyNames)) 156 | 157 | /// Adds an authorization policy to the endpoint(s). 158 | [] 159 | member inline _.authorization([] build: BuildEndpoints, policy: AuthorizationPolicy) = 160 | BuildEndpoints(fun group -> build.Invoke(group).RequireAuthorization(policy)) 161 | 162 | /// Adds an new authorization policy configured by a callback to the endpoint(s). 163 | [] 164 | member inline _.authorization([] build: BuildEndpoints, [] configurePolicy: AuthorizationPolicyBuilder -> unit) = 165 | BuildEndpoints(fun group -> build.Invoke(group).RequireAuthorization(fun x -> configurePolicy x)) 166 | 167 | /// Adds authorization policies with the specified Microsoft.AspNetCore.Authorization.IAuthorizeData to the endpoint(s). 168 | [] 169 | member inline _.authorization([] build: BuildEndpoints, [] authorizeData: IAuthorizeData[]) = 170 | BuildEndpoints(fun group -> build.Invoke(group).RequireAuthorization(authorizeData)) 171 | 172 | /// Adds the specified rate limiting policy to the endpoint(s). 173 | [] 174 | member inline _.rateLimiting([] build: BuildEndpoints, policy: IRateLimiterPolicy<_>) = 175 | BuildEndpoints(fun group -> build.Invoke(group).RequireRateLimiting(policy)) 176 | 177 | /// Adds the specified rate limiting policy to the endpoint(s). 178 | [] 179 | member inline _.rateLimiting([] build: BuildEndpoints, policyName: string) = 180 | BuildEndpoints(fun group -> build.Invoke(group).RequireRateLimiting(policyName)) 181 | 182 | /// 183 | /// Disables rate limiting on the endpoint(s). 184 | /// 185 | /// Will skip both the global limiter, and any endpoint-specific limiters that apply to the endpoint(s). 186 | [] 187 | member inline _.disableRateLimiting([] build: BuildEndpoints) = 188 | BuildEndpoints(fun group -> build.Invoke(group).DisableRateLimiting()) 189 | -------------------------------------------------------------------------------- /Fun.AspNetCore/EndpointCEBuilder.fs: -------------------------------------------------------------------------------- 1 | namespace Fun.AspNetCore 2 | 3 | open System 4 | open Microsoft.OpenApi.Models 5 | open Microsoft.AspNetCore.Http 6 | open Microsoft.AspNetCore.Builder 7 | open Microsoft.AspNetCore.Authorization 8 | open Microsoft.AspNetCore.RateLimiting 9 | open Microsoft.Extensions.DependencyInjection 10 | open Microsoft.AspNetCore.OutputCaching 11 | open Microsoft.AspNetCore.Cors.Infrastructure 12 | open Fun.AspNetCore.Internal 13 | 14 | 15 | type EndpointCEBuilder(methods: string list, pattern: string) = 16 | 17 | static member val GetMethods = [ HttpMethods.Get ] 18 | static member val PutMethods = [ HttpMethods.Put ] 19 | static member val PostMethods = [ HttpMethods.Post ] 20 | static member val DeleteMethods = [ HttpMethods.Delete ] 21 | static member val PatchMethods = [ HttpMethods.Patch ] 22 | 23 | 24 | member _.Methods = methods 25 | member _.Pattern = pattern 26 | 27 | member inline this.Build([] build: BuildEndpoint, handler: Delegate) = 28 | BuildRoute(fun group -> 29 | let route = group.MapMethods(this.Pattern, this.Methods, handler) 30 | build.Invoke(route) 31 | ) 32 | 33 | 34 | member inline _.Run([] fn: BuildRoute) = BuildRoute(fun x -> fn.Invoke(x)) 35 | 36 | member inline this.Run([] fn: BuildEndpoint) = this.Build(fn, Func<_>(fun () -> Results.Ok())) 37 | 38 | 39 | member inline _.Zero() = BuildEndpoint(fun x -> x) 40 | 41 | 42 | member inline _.Yield(_: unit) = BuildEndpoint(fun x -> x) 43 | member inline _.Delay([] fn: unit -> BuildEndpoint) = BuildEndpoint(fun x -> fn().Invoke x) 44 | 45 | member inline this.Yield(x: IResult) = BuildRoute(fun group -> group.MapMethods(this.Pattern, this.Methods, Func<_>(fun () -> x))) 46 | member inline _.Delay([] fn: unit -> BuildRoute) = BuildRoute(fun r -> fn().Invoke(r)) 47 | member inline _.Combine([] buildEndpoint: BuildEndpoint, [] buildRoute: BuildRoute) = 48 | BuildRoute(fun group -> 49 | let route = buildRoute.Invoke(group) 50 | buildEndpoint.Invoke(route) |> ignore 51 | route 52 | ) 53 | 54 | 55 | member inline this.For([] builder: BuildEndpoint, [] fn: unit -> BuildRoute) = this.Combine(builder, fn ()) 56 | 57 | 58 | /// Give a function to configure the endpoint. 59 | [] 60 | member inline _.set([] build: BuildEndpoint, [] fn: RouteHandlerBuilder -> RouteHandlerBuilder) = 61 | BuildEndpoint(fun route -> fn (build.Invoke(route))) 62 | 63 | 64 | /// Registers a filter onto the route handler. 65 | [] 66 | member inline _.filter([] build: BuildEndpoint, filter: IEndpointFilter) = 67 | BuildEndpoint(fun route -> build.Invoke(route).AddEndpointFilter(filter)) 68 | 69 | 70 | /// Adds the Microsoft.AspNetCore.Routing.IExcludeFromDescriptionMetadata to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 71 | [] 72 | member inline _.excludeFromDescription([] build: BuildEndpoint) = 73 | BuildEndpoint(fun route -> build.Invoke(route).ExcludeFromDescription()) 74 | 75 | 76 | /// Adds a CORS policy with the specified name to the endpoint(s). 77 | [] 78 | member inline _.requiredCors([] build: BuildEndpoint, policyName: string) = 79 | BuildEndpoint(fun route -> build.Invoke(route).RequireCors(policyName)) 80 | 81 | /// Adds the specified CORS policy to the endpoint(s). 82 | [] 83 | member inline _.requiredCors([] build: BuildEndpoint, [] config: CorsPolicyBuilder -> CorsPolicyBuilder) = 84 | BuildEndpoint(fun route -> build.Invoke(route).RequireCors(fun x -> config x |> ignore)) 85 | 86 | 87 | /// Requires that endpoints match one of the specified hosts during routing. 88 | [] 89 | member inline _.requireHost([] build: BuildEndpoint, [] hosts: string[]) = 90 | BuildEndpoint(fun route -> build.Invoke(route).RequireHost(hosts)) 91 | 92 | 93 | /// Adds Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 94 | [] 95 | member inline _.accepts([] build: BuildEndpoint, contentType: string, [] additionalContentTypes: string[]) = 96 | BuildEndpoint(fun route -> build.Invoke(route).Accepts(contentType, additionalContentTypes)) 97 | 98 | /// Adds Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 99 | [] 100 | member inline _.accepts 101 | ( 102 | [] build: BuildEndpoint, 103 | [] requestType: unit -> Type, 104 | contentType: string, 105 | [] additionalContentTypes: string[] 106 | ) = 107 | BuildEndpoint(fun route -> build.Invoke(route).Accepts(requestType (), contentType, additionalContentTypes)) 108 | 109 | /// Adds Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 110 | [] 111 | member inline _.acceptsOptional([] build: BuildEndpoint, contentType: string, [] additionalContentTypes: string[]) = 112 | BuildEndpoint(fun route -> build.Invoke(route).Accepts(true, contentType, additionalContentTypes)) 113 | 114 | /// Adds Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 115 | [] 116 | member inline _.acceptsOptional 117 | ( 118 | [] build: BuildEndpoint, 119 | [] requestType: unit -> Type, 120 | contentType: string, 121 | [] additionalContentTypes: string[] 122 | ) = 123 | BuildEndpoint(fun route -> build.Invoke(route).Accepts(requestType (), true, contentType, additionalContentTypes)) 124 | 125 | 126 | /// Adds the Microsoft.AspNetCore.Routing.IEndpointNameMetadata to the Metadata collection for all endpoints produced on the target Microsoft.AspNetCore.Builder.IEndpointConventionBuilder given the endpointName. The Microsoft.AspNetCore.Routing.IEndpointNameMetadata on the endpoint is used for link generation and is treated as the operation ID in the given endpoint's OpenAPI specification. 127 | [] 128 | member inline _.name([] build: BuildEndpoint, name: string) = BuildEndpoint(fun route -> build.Invoke(route).WithName(name)) 129 | 130 | /// Sets the Microsoft.AspNetCore.Builder.EndpointBuilder.DisplayName to the provided displayName for all builders created by builder. 131 | [] 132 | member inline _.displayName([] build: BuildEndpoint, name: string) = 133 | BuildEndpoint(fun route -> build.Invoke(route).WithDisplayName(name)) 134 | 135 | /// Sets the Microsoft.AspNetCore.Routing.EndpointGroupNameAttribute for all endpoints produced on the target Microsoft.AspNetCore.Builder.IEndpointConventionBuilder given the endpointGroupName. The Microsoft.AspNetCore.Routing.IEndpointGroupNameMetadata on the endpoint is used to set the endpoint's GroupName in the OpenAPI specification. 136 | [] 137 | member inline _.groupName([] build: BuildEndpoint, name: string) = 138 | BuildEndpoint(fun route -> build.Invoke(route).WithGroupName(name)) 139 | 140 | /// Adds Microsoft.AspNetCore.Http.Metadata.IEndpointDescriptionMetadata to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 141 | [] 142 | member inline _.description([] build: BuildEndpoint, description: string) = 143 | BuildEndpoint(fun route -> build.Invoke(route).WithDescription(description)) 144 | 145 | /// Adds Microsoft.AspNetCore.Http.Metadata.IEndpointSummaryMetadata to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 146 | [] 147 | member inline _.summary([] build: BuildEndpoint, summary: string) = 148 | BuildEndpoint(fun route -> build.Invoke(route).WithSummary(summary)) 149 | 150 | /// 151 | /// Adds the to for all endpoints 152 | /// produced by . 153 | /// 154 | /// 155 | /// The OpenAPI specification supports a tags classification to categorize operations 156 | /// into related groups. These tags are typically included in the generated specification 157 | /// and are typically used to group operations by tags in the UI. 158 | /// 159 | [] 160 | member inline _.tags([] build: BuildEndpoint, [] tags: string[]) = 161 | BuildEndpoint(fun route -> build.Invoke(route).WithTags(tags)) 162 | 163 | 164 | /// Adds the provided metadata items to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all builders produced by builder. 165 | [] 166 | member inline _.metas([] build: BuildEndpoint, metas: obj[]) = BuildEndpoint(fun route -> build.Invoke(route).WithMetadata(metas)) 167 | 168 | 169 | /// Adds an Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 170 | [] 171 | member inline _.produces([] build: BuildEndpoint, [] getType: unit -> Type, statusCode) = 172 | BuildEndpoint(fun route -> build.Invoke(route).Produces(statusCode, responseType = getType (), contentType = "application/json")) 173 | 174 | /// Adds an Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 175 | [] 176 | member inline _.produces([] build: BuildEndpoint, [] getType: unit -> Type, statusCode, contentType) = 177 | BuildEndpoint(fun route -> build.Invoke(route).Produces(statusCode, responseType = getType (), contentType = contentType)) 178 | 179 | /// Adds an Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 180 | [] 181 | member inline _.produces([] build: BuildEndpoint, statusCode, contentType) = 182 | BuildEndpoint(fun route -> build.Invoke(route).Produces(statusCode, contentType)) 183 | 184 | /// Adds an Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata with a Microsoft.AspNetCore.Mvc.ProblemDetails type to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 185 | [] 186 | member inline _.producesProblem([] build: BuildEndpoint, statusCode) = 187 | BuildEndpoint(fun route -> build.Invoke(route).ProducesProblem(statusCode)) 188 | 189 | /// Adds an Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata with a Microsoft.AspNetCore.Mvc.ProblemDetails type to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 190 | [] 191 | member inline _.producesProblem([] build: BuildEndpoint, statusCode, contentType) = 192 | BuildEndpoint(fun route -> build.Invoke(route).ProducesProblem(statusCode, contentType)) 193 | 194 | /// Adds an Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata with a Microsoft.AspNetCore.Http.HttpValidationProblemDetails type to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 195 | [] 196 | member inline _.producesValidationProblem([] build: BuildEndpoint) = 197 | BuildEndpoint(fun route -> build.Invoke(route).ProducesValidationProblem()) 198 | 199 | /// Adds an Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata with a Microsoft.AspNetCore.Http.HttpValidationProblemDetails type to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 200 | [] 201 | member inline _.producesValidationProblem([] build: BuildEndpoint, statusCode) = 202 | BuildEndpoint(fun route -> build.Invoke(route).ProducesValidationProblem(statusCode)) 203 | 204 | /// Adds an Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata with a Microsoft.AspNetCore.Http.HttpValidationProblemDetails type to Microsoft.AspNetCore.Builder.EndpointBuilder.Metadata for all endpoints produced by builder. 205 | [] 206 | member inline _.producesValidationProblem([] build: BuildEndpoint, statusCode, contentType) = 207 | BuildEndpoint(fun route -> build.Invoke(route).ProducesValidationProblem(statusCode, contentType)) 208 | 209 | /// Adds an OpenAPI annotation to Microsoft.AspNetCore.Http.Endpoint.Metadata associated with the current endpoint. 210 | [] 211 | member inline _.openApi([] build: BuildEndpoint) = BuildEndpoint(fun route -> build.Invoke(route).WithOpenApi()) 212 | 213 | /// Adds an OpenAPI annotation to Microsoft.AspNetCore.Http.Endpoint.Metadata associated with the current endpoint. 214 | [] 215 | member inline _.openApi([] build: BuildEndpoint, [] configOperation: OpenApiOperation -> unit) = 216 | BuildEndpoint(fun route -> 217 | build 218 | .Invoke(route) 219 | .WithOpenApi(fun x -> 220 | configOperation x 221 | x 222 | ) 223 | ) 224 | 225 | /// Allows anonymous access to the endpoint by adding Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute to the endpoint metadata. This will bypass all authorization checks for the endpoint including the default authorization policy and fallback authorization policy. 226 | [] 227 | member inline _.anonymous([] build: BuildEndpoint) = BuildEndpoint(fun route -> build.Invoke(route).AllowAnonymous()) 228 | 229 | /// Adds the default authorization policy to the endpoint(s). 230 | [] 231 | member inline _.authorization([] build: BuildEndpoint) = BuildEndpoint(fun route -> build.Invoke(route).RequireAuthorization()) 232 | 233 | /// Adds authorization policies with the specified names to the endpoint(s). 234 | [] 235 | member inline _.authorization([] build: BuildEndpoint, policyNames: string[]) = 236 | BuildEndpoint(fun route -> build.Invoke(route).RequireAuthorization(policyNames)) 237 | 238 | /// Adds an authorization policy to the endpoint(s). 239 | [] 240 | member inline _.authorization([] build: BuildEndpoint, policy: AuthorizationPolicy) = 241 | BuildEndpoint(fun route -> build.Invoke(route).RequireAuthorization(policy)) 242 | 243 | /// Adds an new authorization policy configured by a callback to the endpoint(s). 244 | [] 245 | member inline _.authorization([] build: BuildEndpoint, [] configurePolicy: AuthorizationPolicyBuilder -> unit) = 246 | BuildEndpoint(fun route -> build.Invoke(route).RequireAuthorization(fun x -> configurePolicy x)) 247 | 248 | /// Adds authorization policies with the specified Microsoft.AspNetCore.Authorization.IAuthorizeData to the endpoint(s). 249 | [] 250 | member inline _.authorization([] build: BuildEndpoint, [] authorizeData: IAuthorizeData[]) = 251 | BuildEndpoint(fun route -> build.Invoke(route).RequireAuthorization(authorizeData)) 252 | 253 | /// Adds the specified rate limiting policy to the endpoint(s). 254 | [] 255 | member inline _.rateLimiting([] build: BuildEndpoint, policy: IRateLimiterPolicy<_>) = 256 | BuildEndpoint(fun route -> build.Invoke(route).RequireRateLimiting(policy)) 257 | 258 | /// Adds the specified rate limiting policy to the endpoint(s). 259 | [] 260 | member inline _.rateLimiting([] build: BuildEndpoint, policyName: string) = 261 | BuildEndpoint(fun route -> build.Invoke(route).RequireRateLimiting(policyName)) 262 | 263 | /// 264 | /// Disables rate limiting on the endpoint(s). 265 | /// 266 | /// Will skip both the global limiter, and any endpoint-specific limiters that apply to the endpoint(s). 267 | [] 268 | member inline _.disableRateLimiting([] build: BuildEndpoint) = 269 | BuildEndpoint(fun route -> build.Invoke(route).DisableRateLimiting()) 270 | 271 | 272 | [] 273 | member inline _.cacheOutput([] build: BuildEndpoint) = BuildEndpoint(fun route -> build.Invoke(route).CacheOutput()) 274 | 275 | [] 276 | member inline _.cacheOutput([] build: BuildEndpoint, policyName: string) = 277 | BuildEndpoint(fun route -> build.Invoke(route).CacheOutput(policyName)) 278 | 279 | [] 280 | member inline _.cacheOutput([] build: BuildEndpoint, policy: IOutputCachePolicy) = 281 | BuildEndpoint(fun route -> build.Invoke(route).CacheOutput(policy)) 282 | 283 | [] 284 | member inline _.cacheOutput 285 | ( 286 | [] build: BuildEndpoint, 287 | [] configurePolicy: OutputCachePolicyBuilder -> OutputCachePolicyBuilder 288 | ) = 289 | BuildEndpoint(fun route -> build.Invoke(route).CacheOutput(fun x -> configurePolicy x |> ignore)) 290 | 291 | 292 | [] 293 | member inline _.responseCache([] build: BuildEndpoint, time: TimeSpan) = 294 | BuildEndpoint(fun route -> 295 | build 296 | .Invoke(route) 297 | .AddEndpointFilter( 298 | { new IEndpointFilter with 299 | member _.InvokeAsync(ctx, next) = 300 | if ctx.HttpContext.Response.HasStarted then 301 | failwith "Cannot set headers when they are already sent to client" 302 | ctx.HttpContext.Response.Headers.CacheControl <- $"max-age:{time.TotalSeconds}" 303 | next.Invoke ctx 304 | } 305 | ) 306 | ) 307 | 308 | 309 | [] 310 | member inline this.handle([] build: BuildEndpoint, handler: Func<'T, _>) = 311 | if typeof<'T> = typeof then 312 | this.Build(build, Func<_>(fun () -> handler.Invoke(unbox<'T> ()))) 313 | else 314 | this.Build(build, handler) 315 | 316 | /// Provide a Func delegate for the endpoints. You can only call this one time. 317 | [] 318 | member inline this.handle([] build: BuildEndpoint, handler: Func<_, _, _>) = this.Build(build, handler) 319 | 320 | /// Provide a Func delegate for the endpoints. You can only call this one time. 321 | [] 322 | member inline this.handle([] build: BuildEndpoint, handler: Func<_, _, _, _>) = this.Build(build, handler) 323 | 324 | /// Provide a Func delegate for the endpoints. You can only call this one time. 325 | [] 326 | member inline this.handle([] build: BuildEndpoint, handler: Func<_, _, _, _, _>) = this.Build(build, handler) 327 | 328 | /// Provide a Func delegate for the endpoints. You can only call this one time. 329 | [] 330 | member inline this.handle([] build: BuildEndpoint, handler: Func<_, _, _, _, _, _>) = this.Build(build, handler) 331 | 332 | /// Provide a Func delegate for the endpoints. You can only call this one time. 333 | [] 334 | member inline this.handle([] build: BuildEndpoint, handler: Func<_, _, _, _, _, _, _>) = this.Build(build, handler) 335 | --------------------------------------------------------------------------------