Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Alpha] Add new route functions (...WithExtensions) #634

Merged
merged 7 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -3490,8 +3490,11 @@ The following routing functions are available as part of the `Giraffe.EndpointRo

- `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`, `TRACE`, `CONNECT`
- `route`
- `routeWithExtensions` (*alpha*)
- `routef`
- `routefWithExtensions` (*alpha*)
- `subRoute`
- `subRouteWithExtensions` (*alpha*)

The `route`, `routef` and `subRoute` handlers are all case-insensitive. Other handlers such as `routex`, `subRoutef` or `choose` are not supported by the `Giraffe.EndpointRouting` module.

Expand Down Expand Up @@ -3525,6 +3528,39 @@ let myHandler (foo : int, bar : string) : HttpHandler =

For more information about ASP.NET Core Endpoint Routing please refer to the [official documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-5.0).

##### ALPHA :: Endpoint Routing Functions with Extensions

+ Note that this feature is currently in **alpha**, and major changes are expected.

ASP.NET Core provides several "extension" functions which can be used to fine-tune the HTTP handler behaviour. For example, there's the [Rate limiting](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit) and [Output caching](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/output) middlewares.

By using the Endpoint Routing module we can leverage this along with the `...WithExtensions` routing functions: `routeWithExtensions`, `routefWithExtensions` and `subRouteWithExtensions`.

Basically, whenever you decide to use a routing function variant with `...WithExtensions` you're required to provide as the first parameter a function that obbeys the `ConfigureEndpoint` type definition:

```fsharp
// Note: IEndpointConventionBuilder is a shorter version of Microsoft.AspNetCore.Builder.IEndpointConventionBuilder
type ConfigureEndpoint = IEndpointConventionBuilder -> IEndpointConventionBuilder
```

And you can use it like this:

```fsharp
let MY_RATE_LIMITER = "fixed"

let endpoints: list<Endpoint> =
[
GET [
routeWithExtensions (fun eb -> eb.RequireRateLimiting MY_RATE_LIMITER) "/rate-limit" (text "Hello World")
route "/no-rate-limit" (text "Hello World: No Rate Limit!")
]
]
```

In this example, we're using the ASP.NET [Rate limiting](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit) middleware for the path `/rate-limit`, and not using it for `/no-rate-limit`. If you'd like to test it, check the sample at the [official repository](https://github.com/giraffe-fsharp/Giraffe) under the path *samples/RateLimiting/*. There's a `README.md` file with instructions on how to run it locally.

Note that for those extensions to work properly, you'll probably need to make additional changes to the server. Please check the official extension documentation page to know more about this.

### TokenRouter

The `Giraffe.TokenRouter` NuGet package exposes an alternative routing `HttpHandler` which is based on top of a [Radix Tree](https://en.wikipedia.org/wiki/Radix_tree). Several routing handlers (e.g.: `routef` and `subRoute`) have been overridden in such a way that path matching and value parsing are significantly faster than using the basic `choose` function.
Expand Down
15 changes: 15 additions & 0 deletions Giraffe.sln
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "NewtonsoftJson", "samples\N
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "GlobalRateLimiting", "samples\GlobalRateLimiting\GlobalRateLimiting.fsproj", "{C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "RateLimiting", "samples\RateLimiting\RateLimiting.fsproj", "{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -118,6 +120,18 @@ Global
{C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Release|x64.Build.0 = Release|Any CPU
{C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Release|x86.ActiveCfg = Release|Any CPU
{C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Release|x86.Build.0 = Release|Any CPU
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|x64.ActiveCfg = Debug|Any CPU
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|x64.Build.0 = Debug|Any CPU
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|x86.ActiveCfg = Debug|Any CPU
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|x86.Build.0 = Debug|Any CPU
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|Any CPU.Build.0 = Release|Any CPU
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|x64.ActiveCfg = Release|Any CPU
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|x64.Build.0 = Release|Any CPU
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|x86.ActiveCfg = Release|Any CPU
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -133,5 +147,6 @@ Global
{FA102AC4-4608-42F9-86C1-1472B416A76E} = {9E6451FB-26E0-4AE4-A469-847F9602E999}
{A08230F1-DA24-4059-A7F9-4743B36DD3E9} = {9E6451FB-26E0-4AE4-A469-847F9602E999}
{C5E71E00-4DD0-4ED8-B781-7DB63B7565E4} = {9E6451FB-26E0-4AE4-A469-847F9602E999}
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9} = {9E6451FB-26E0-4AE4-A469-847F9602E999}
EndGlobalSection
EndGlobal
21 changes: 21 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
Release Notes
=============

## 8.0.0-alpha-001 - 2025-02-11

With this release, we're improving the codebase a bit by fixing warnings triggered by Ionide.Analyzers, and adding .NET 9 as a target framework to the project.

Other than that, we're adding new router functions for the `EndpointRouting` module which will let the user interact with Giraffe's `ConfigureEndpoint` directly. This will let you use Asp.Net extensions directly, like rate limiting, response caching, etc. Just remember its type definition:

```fsharp
type ConfigureEndpoint = IEndpointConventionBuilder -> IEndpointConventionBuilder
```

And here we have the list of PRs related to this release:

- [Add GitHub dependabot configuration](https://github.com/giraffe-fsharp/Giraffe/pull/621) - Credits @64J0
- [Add global rate limiting sample](https://github.com/giraffe-fsharp/Giraffe/pull/622) - Credits @64J0
- [Add OpenApi section to the documentation](https://github.com/giraffe-fsharp/Giraffe/pull/624) - Credits @64J0
- [Add AssemblyVersion attribute](https://github.com/giraffe-fsharp/Giraffe/pull/629) - Credits @64J0
- [Add more links](https://github.com/giraffe-fsharp/Giraffe/pull/633) - Credits @64J0
- [Code scanning fix patches](https://github.com/giraffe-fsharp/Giraffe/pull/638) - Credits @64J0
- [Add .NET 9 as target framework, fine-tune dependabot, update CI and clean tests removing .NET 6/7 from target frameworks](https://github.com/giraffe-fsharp/Giraffe/pull/639) - Credits @64J0
- [[Alpha] Add Endpoint routing functions ...WithExtensions](https://github.com/giraffe-fsharp/Giraffe/pull/634) - Credits @64J0

## 7.0.2 - 2024-10-16

Combination of the tags:
Expand Down
56 changes: 56 additions & 0 deletions samples/RateLimiting/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
open System
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting
open Giraffe
open Giraffe.EndpointRouting
open Microsoft.AspNetCore.RateLimiting
open System.Threading.RateLimiting

let MY_RATE_LIMITER = "fixed"

let endpoints: list<Endpoint> =
[
GET [
routeWithExtensions (fun eb -> eb.RequireRateLimiting MY_RATE_LIMITER) "/rate-limit" (text "Hello World")
route "/no-rate-limit" (text "Hello World: No Rate Limit!")
]
]

let notFoundHandler = text "Not Found" |> RequestErrors.notFound

let configureApp (appBuilder: IApplicationBuilder) =
appBuilder.UseRouting().UseRateLimiter().UseGiraffe(endpoints).UseGiraffe(notFoundHandler)

let configureServices (services: IServiceCollection) =
// From https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?view=aspnetcore-8.0#fixed-window-limiter
let configureRateLimiter (rateLimiterOptions: RateLimiterOptions) =
rateLimiterOptions.RejectionStatusCode <- StatusCodes.Status429TooManyRequests

rateLimiterOptions.AddFixedWindowLimiter(
policyName = MY_RATE_LIMITER,
configureOptions =
(fun (options: FixedWindowRateLimiterOptions) ->
options.PermitLimit <- 10
options.Window <- TimeSpan.FromSeconds(int64 12)
options.QueueProcessingOrder <- QueueProcessingOrder.OldestFirst
options.QueueLimit <- 1
)
)
|> ignore

services.AddRateLimiter(configureRateLimiter).AddRouting().AddGiraffe()
|> ignore

[<EntryPoint>]
let main args =
let builder = WebApplication.CreateBuilder(args)
configureServices builder.Services

let app = builder.Build()

configureApp app
app.Run()

0
20 changes: 20 additions & 0 deletions samples/RateLimiting/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Rate Limiting Sample

This sample project shows how one can configure ASP.NET's built-in [rate limiting middleware](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?view=aspnetcore-8.0).

Notice that this rate limiting configuration is very simple, and for real life scenarios you'll need to figure out what is the best strategy to use for your server.

To make it easier to test this project locally, and see the rate limiting middleware working, you can use the `rate-limiting-test.fsx` script:

```bash
# start the server
dotnet run .
# if you want to keep using the same terminal, just start this process in the background

# then, you can use this script to test the server, and confirm that the rate-limiting
# middleware is really working
dotnet fsi rate-limiting-test.fsx

# to run with the DEBUG flag active
dotnet fsi --define:DEBUG rate-limiting-test.fsx
```
16 changes: 16 additions & 0 deletions samples/RateLimiting/RateLimiting.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../../src/Giraffe/Giraffe.fsproj" />
</ItemGroup>

</Project>
57 changes: 57 additions & 0 deletions samples/RateLimiting/rate-limiting-test.fsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
open System
open System.Net.Http

let request = new HttpClient(BaseAddress = new Uri("http://localhost:5000"))

let program () =
async {
let! reqResult1 =
seq { 1..100 }
|> Seq.map (fun _ -> request.GetAsync "/no-rate-limit" |> Async.AwaitTask)
|> Async.Parallel

reqResult1
#if DEBUG
|> Seq.iteri (fun i response ->
printfn "\nResponse %i status code: %A" i response.StatusCode

let responseReader = new StreamReader(response.Content.ReadAsStream())
printfn "Response %i content: %A" i (responseReader.ReadToEnd())
)
#else
|> Seq.groupBy (fun response -> response.StatusCode)
|> Seq.iter (fun (group) ->
let key, seqRes = group
printfn "Quantity of requests with status code %A: %i" (key) (Seq.length seqRes)
)
#endif

printfn "\nWith rate limit now...\n"

let! reqResult2 =
seq { 1..100 }
|> Seq.map (fun _ -> request.GetAsync "/rate-limit" |> Async.AwaitTask)
|> Async.Parallel

reqResult2
#if DEBUG
|> Seq.iteri (fun i response ->
printfn "\nResponse %i status code: %A" i response.StatusCode

let responseReader = new StreamReader(response.Content.ReadAsStream())
printfn "Response %i content: %A" i (responseReader.ReadToEnd())
)
#else
|> Seq.groupBy (fun response -> response.StatusCode)
|> Seq.iter (fun (group) ->
let key, seqRes = group
printfn "Quantity of requests with status code %A: %i\n" (key) (Seq.length seqRes)
)
#endif
}

#time

program () |> Async.RunSynchronously

#time
2 changes: 1 addition & 1 deletion samples/ResponseCachingApp/ResponseCachingApp.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
Expand Down
28 changes: 23 additions & 5 deletions src/Giraffe/EndpointRouting.fs
Original file line number Diff line number Diff line change
Expand Up @@ -232,19 +232,37 @@ module Routers =
let TRACE = applyHttpVerbToEndpoints TRACE
let CONNECT = applyHttpVerbToEndpoints CONNECT

let routeWithExtensions (configureEndpoint: ConfigureEndpoint) (path: string) (handler: HttpHandler) : Endpoint =
SimpleEndpoint(HttpVerb.NotSpecified, path, handler, configureEndpoint)

let route (path: string) (handler: HttpHandler) : Endpoint =
SimpleEndpoint(HttpVerb.NotSpecified, path, handler, id)
routeWithExtensions (id) (path) (handler)

let routef (path: PrintfFormat<_, _, _, _, 'T>) (routeHandler: 'T -> HttpHandler) : Endpoint =
let routefWithExtensions
(configureEndpoint: ConfigureEndpoint)
(path: PrintfFormat<_, _, _, _, 'T>)
(routeHandler: 'T -> HttpHandler)
: Endpoint =
let template, mappings = RouteTemplateBuilder.convertToRouteTemplate path

let boxedHandler (o: obj) =
let t = o :?> 'T
routeHandler t

TemplateEndpoint(HttpVerb.NotSpecified, template, mappings, boxedHandler, id)
TemplateEndpoint(HttpVerb.NotSpecified, template, mappings, boxedHandler, configureEndpoint)

let routef (path: PrintfFormat<_, _, _, _, 'T>) (routeHandler: 'T -> HttpHandler) : Endpoint =
routefWithExtensions (id) (path) (routeHandler)

let subRouteWithExtensions
(configureEndpoint: ConfigureEndpoint)
(path: string)
(endpoints: Endpoint list)
: Endpoint =
NestedEndpoint(path, endpoints, configureEndpoint)

let subRoute (path: string) (endpoints: Endpoint list) : Endpoint = NestedEndpoint(path, endpoints, id)
let subRoute (path: string) (endpoints: Endpoint list) : Endpoint =
subRouteWithExtensions (id) (path) (endpoints)

let rec applyBefore (httpHandler: HttpHandler) (endpoint: Endpoint) =
match endpoint with
Expand All @@ -267,7 +285,7 @@ module Routers =
| NestedEndpoint(t, lst, ce) -> NestedEndpoint(t, lst, ce >> f)
| MultiEndpoint(lst) -> MultiEndpoint(List.map (configureEndpoint f) lst)

let addMetadata (metadata: obj) (endpoint: Endpoint) =
let addMetadata (metadata: obj) (endpoint: Endpoint) : Endpoint =
endpoint |> configureEndpoint _.WithMetadata(metadata)

// ---------------------------
Expand Down
2 changes: 1 addition & 1 deletion src/Giraffe/Giraffe.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<!-- General -->
<AssemblyName>Giraffe</AssemblyName>
<AssemblyVersion>7.0.2</AssemblyVersion>
<AssemblyVersion>8.0.0-alpha-001</AssemblyVersion>
<Description>A native functional ASP.NET Core web framework for F# developers.</Description>
<Copyright>Copyright 2020 Dustin Moris Gorski</Copyright>
<Authors>Dustin Moris Gorski and contributors</Authors>
Expand Down
Loading