Skip to content

Commit 880dcd7

Browse files
authored
[Alpha] Add new route functions (...WithExtensions) (#634)
* add new endpoint routing functions to deal with asp.net extensions * add new automated tests for basic checks of the new routing functions * add new sample for asp.net rate limiting * add new sample to the sln * add documentation for the new routing functions * add release notes placeholder * update assembly version and remove comment
1 parent 0376ef4 commit 880dcd7

11 files changed

+347
-7
lines changed

DOCUMENTATION.md

+36
Original file line numberDiff line numberDiff line change
@@ -3490,8 +3490,11 @@ The following routing functions are available as part of the `Giraffe.EndpointRo
34903490

34913491
- `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`, `TRACE`, `CONNECT`
34923492
- `route`
3493+
- `routeWithExtensions` (*alpha*)
34933494
- `routef`
3495+
- `routefWithExtensions` (*alpha*)
34943496
- `subRoute`
3497+
- `subRouteWithExtensions` (*alpha*)
34953498

34963499
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.
34973500

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

35263529
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).
35273530

3531+
##### ALPHA :: Endpoint Routing Functions with Extensions
3532+
3533+
+ Note that this feature is currently in **alpha**, and major changes are expected.
3534+
3535+
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.
3536+
3537+
By using the Endpoint Routing module we can leverage this along with the `...WithExtensions` routing functions: `routeWithExtensions`, `routefWithExtensions` and `subRouteWithExtensions`.
3538+
3539+
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:
3540+
3541+
```fsharp
3542+
// Note: IEndpointConventionBuilder is a shorter version of Microsoft.AspNetCore.Builder.IEndpointConventionBuilder
3543+
type ConfigureEndpoint = IEndpointConventionBuilder -> IEndpointConventionBuilder
3544+
```
3545+
3546+
And you can use it like this:
3547+
3548+
```fsharp
3549+
let MY_RATE_LIMITER = "fixed"
3550+
3551+
let endpoints: list<Endpoint> =
3552+
[
3553+
GET [
3554+
routeWithExtensions (fun eb -> eb.RequireRateLimiting MY_RATE_LIMITER) "/rate-limit" (text "Hello World")
3555+
route "/no-rate-limit" (text "Hello World: No Rate Limit!")
3556+
]
3557+
]
3558+
```
3559+
3560+
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.
3561+
3562+
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.
3563+
35283564
### TokenRouter
35293565

35303566
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.

Giraffe.sln

+15
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "NewtonsoftJson", "samples\N
3636
EndProject
3737
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "GlobalRateLimiting", "samples\GlobalRateLimiting\GlobalRateLimiting.fsproj", "{C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}"
3838
EndProject
39+
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "RateLimiting", "samples\RateLimiting\RateLimiting.fsproj", "{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}"
40+
EndProject
3941
Global
4042
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4143
Debug|Any CPU = Debug|Any CPU
@@ -118,6 +120,18 @@ Global
118120
{C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Release|x64.Build.0 = Release|Any CPU
119121
{C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Release|x86.ActiveCfg = Release|Any CPU
120122
{C5E71E00-4DD0-4ED8-B781-7DB63B7565E4}.Release|x86.Build.0 = Release|Any CPU
123+
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
124+
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
125+
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|x64.ActiveCfg = Debug|Any CPU
126+
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|x64.Build.0 = Debug|Any CPU
127+
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|x86.ActiveCfg = Debug|Any CPU
128+
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Debug|x86.Build.0 = Debug|Any CPU
129+
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
130+
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|Any CPU.Build.0 = Release|Any CPU
131+
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|x64.ActiveCfg = Release|Any CPU
132+
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|x64.Build.0 = Release|Any CPU
133+
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|x86.ActiveCfg = Release|Any CPU
134+
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9}.Release|x86.Build.0 = Release|Any CPU
121135
EndGlobalSection
122136
GlobalSection(SolutionProperties) = preSolution
123137
HideSolutionNode = FALSE
@@ -133,5 +147,6 @@ Global
133147
{FA102AC4-4608-42F9-86C1-1472B416A76E} = {9E6451FB-26E0-4AE4-A469-847F9602E999}
134148
{A08230F1-DA24-4059-A7F9-4743B36DD3E9} = {9E6451FB-26E0-4AE4-A469-847F9602E999}
135149
{C5E71E00-4DD0-4ED8-B781-7DB63B7565E4} = {9E6451FB-26E0-4AE4-A469-847F9602E999}
150+
{B6A90A80-FB51-48D6-8273-DA651CE2F3F9} = {9E6451FB-26E0-4AE4-A469-847F9602E999}
136151
EndGlobalSection
137152
EndGlobal

RELEASE_NOTES.md

+21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
Release Notes
22
=============
33

4+
## 8.0.0-alpha-001 - 2025-02-11
5+
6+
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.
7+
8+
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:
9+
10+
```fsharp
11+
type ConfigureEndpoint = IEndpointConventionBuilder -> IEndpointConventionBuilder
12+
```
13+
14+
And here we have the list of PRs related to this release:
15+
16+
- [Add GitHub dependabot configuration](https://github.com/giraffe-fsharp/Giraffe/pull/621) - Credits @64J0
17+
- [Add global rate limiting sample](https://github.com/giraffe-fsharp/Giraffe/pull/622) - Credits @64J0
18+
- [Add OpenApi section to the documentation](https://github.com/giraffe-fsharp/Giraffe/pull/624) - Credits @64J0
19+
- [Add AssemblyVersion attribute](https://github.com/giraffe-fsharp/Giraffe/pull/629) - Credits @64J0
20+
- [Add more links](https://github.com/giraffe-fsharp/Giraffe/pull/633) - Credits @64J0
21+
- [Code scanning fix patches](https://github.com/giraffe-fsharp/Giraffe/pull/638) - Credits @64J0
22+
- [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
23+
- [[Alpha] Add Endpoint routing functions ...WithExtensions](https://github.com/giraffe-fsharp/Giraffe/pull/634) - Credits @64J0
24+
425
## 7.0.2 - 2024-10-16
526

627
Combination of the tags:

samples/RateLimiting/Program.fs

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
open System
2+
open Microsoft.AspNetCore.Http
3+
open Microsoft.AspNetCore.Builder
4+
open Microsoft.Extensions.DependencyInjection
5+
open Microsoft.Extensions.Hosting
6+
open Giraffe
7+
open Giraffe.EndpointRouting
8+
open Microsoft.AspNetCore.RateLimiting
9+
open System.Threading.RateLimiting
10+
11+
let MY_RATE_LIMITER = "fixed"
12+
13+
let endpoints: list<Endpoint> =
14+
[
15+
GET [
16+
routeWithExtensions (fun eb -> eb.RequireRateLimiting MY_RATE_LIMITER) "/rate-limit" (text "Hello World")
17+
route "/no-rate-limit" (text "Hello World: No Rate Limit!")
18+
]
19+
]
20+
21+
let notFoundHandler = text "Not Found" |> RequestErrors.notFound
22+
23+
let configureApp (appBuilder: IApplicationBuilder) =
24+
appBuilder.UseRouting().UseRateLimiter().UseGiraffe(endpoints).UseGiraffe(notFoundHandler)
25+
26+
let configureServices (services: IServiceCollection) =
27+
// From https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?view=aspnetcore-8.0#fixed-window-limiter
28+
let configureRateLimiter (rateLimiterOptions: RateLimiterOptions) =
29+
rateLimiterOptions.RejectionStatusCode <- StatusCodes.Status429TooManyRequests
30+
31+
rateLimiterOptions.AddFixedWindowLimiter(
32+
policyName = MY_RATE_LIMITER,
33+
configureOptions =
34+
(fun (options: FixedWindowRateLimiterOptions) ->
35+
options.PermitLimit <- 10
36+
options.Window <- TimeSpan.FromSeconds(int64 12)
37+
options.QueueProcessingOrder <- QueueProcessingOrder.OldestFirst
38+
options.QueueLimit <- 1
39+
)
40+
)
41+
|> ignore
42+
43+
services.AddRateLimiter(configureRateLimiter).AddRouting().AddGiraffe()
44+
|> ignore
45+
46+
[<EntryPoint>]
47+
let main args =
48+
let builder = WebApplication.CreateBuilder(args)
49+
configureServices builder.Services
50+
51+
let app = builder.Build()
52+
53+
configureApp app
54+
app.Run()
55+
56+
0

samples/RateLimiting/README.md

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Rate Limiting Sample
2+
3+
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).
4+
5+
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.
6+
7+
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:
8+
9+
```bash
10+
# start the server
11+
dotnet run .
12+
# if you want to keep using the same terminal, just start this process in the background
13+
14+
# then, you can use this script to test the server, and confirm that the rate-limiting
15+
# middleware is really working
16+
dotnet fsi rate-limiting-test.fsx
17+
18+
# to run with the DEBUG flag active
19+
dotnet fsi --define:DEBUG rate-limiting-test.fsx
20+
```
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net9.0</TargetFramework>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<Compile Include="Program.fs" />
10+
</ItemGroup>
11+
12+
<ItemGroup>
13+
<ProjectReference Include="../../src/Giraffe/Giraffe.fsproj" />
14+
</ItemGroup>
15+
16+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
open System
2+
open System.Net.Http
3+
4+
let request = new HttpClient(BaseAddress = new Uri("http://localhost:5000"))
5+
6+
let program () =
7+
async {
8+
let! reqResult1 =
9+
seq { 1..100 }
10+
|> Seq.map (fun _ -> request.GetAsync "/no-rate-limit" |> Async.AwaitTask)
11+
|> Async.Parallel
12+
13+
reqResult1
14+
#if DEBUG
15+
|> Seq.iteri (fun i response ->
16+
printfn "\nResponse %i status code: %A" i response.StatusCode
17+
18+
let responseReader = new StreamReader(response.Content.ReadAsStream())
19+
printfn "Response %i content: %A" i (responseReader.ReadToEnd())
20+
)
21+
#else
22+
|> Seq.groupBy (fun response -> response.StatusCode)
23+
|> Seq.iter (fun (group) ->
24+
let key, seqRes = group
25+
printfn "Quantity of requests with status code %A: %i" (key) (Seq.length seqRes)
26+
)
27+
#endif
28+
29+
printfn "\nWith rate limit now...\n"
30+
31+
let! reqResult2 =
32+
seq { 1..100 }
33+
|> Seq.map (fun _ -> request.GetAsync "/rate-limit" |> Async.AwaitTask)
34+
|> Async.Parallel
35+
36+
reqResult2
37+
#if DEBUG
38+
|> Seq.iteri (fun i response ->
39+
printfn "\nResponse %i status code: %A" i response.StatusCode
40+
41+
let responseReader = new StreamReader(response.Content.ReadAsStream())
42+
printfn "Response %i content: %A" i (responseReader.ReadToEnd())
43+
)
44+
#else
45+
|> Seq.groupBy (fun response -> response.StatusCode)
46+
|> Seq.iter (fun (group) ->
47+
let key, seqRes = group
48+
printfn "Quantity of requests with status code %A: %i\n" (key) (Seq.length seqRes)
49+
)
50+
#endif
51+
}
52+
53+
#time
54+
55+
program () |> Async.RunSynchronously
56+
57+
#time

samples/ResponseCachingApp/ResponseCachingApp.fsproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net7.0</TargetFramework>
5+
<TargetFramework>net9.0</TargetFramework>
66
</PropertyGroup>
77

88
<ItemGroup>

src/Giraffe/EndpointRouting.fs

+23-5
Original file line numberDiff line numberDiff line change
@@ -232,19 +232,37 @@ module Routers =
232232
let TRACE = applyHttpVerbToEndpoints TRACE
233233
let CONNECT = applyHttpVerbToEndpoints CONNECT
234234

235+
let routeWithExtensions (configureEndpoint: ConfigureEndpoint) (path: string) (handler: HttpHandler) : Endpoint =
236+
SimpleEndpoint(HttpVerb.NotSpecified, path, handler, configureEndpoint)
237+
235238
let route (path: string) (handler: HttpHandler) : Endpoint =
236-
SimpleEndpoint(HttpVerb.NotSpecified, path, handler, id)
239+
routeWithExtensions (id) (path) (handler)
237240

238-
let routef (path: PrintfFormat<_, _, _, _, 'T>) (routeHandler: 'T -> HttpHandler) : Endpoint =
241+
let routefWithExtensions
242+
(configureEndpoint: ConfigureEndpoint)
243+
(path: PrintfFormat<_, _, _, _, 'T>)
244+
(routeHandler: 'T -> HttpHandler)
245+
: Endpoint =
239246
let template, mappings = RouteTemplateBuilder.convertToRouteTemplate path
240247

241248
let boxedHandler (o: obj) =
242249
let t = o :?> 'T
243250
routeHandler t
244251

245-
TemplateEndpoint(HttpVerb.NotSpecified, template, mappings, boxedHandler, id)
252+
TemplateEndpoint(HttpVerb.NotSpecified, template, mappings, boxedHandler, configureEndpoint)
253+
254+
let routef (path: PrintfFormat<_, _, _, _, 'T>) (routeHandler: 'T -> HttpHandler) : Endpoint =
255+
routefWithExtensions (id) (path) (routeHandler)
256+
257+
let subRouteWithExtensions
258+
(configureEndpoint: ConfigureEndpoint)
259+
(path: string)
260+
(endpoints: Endpoint list)
261+
: Endpoint =
262+
NestedEndpoint(path, endpoints, configureEndpoint)
246263

247-
let subRoute (path: string) (endpoints: Endpoint list) : Endpoint = NestedEndpoint(path, endpoints, id)
264+
let subRoute (path: string) (endpoints: Endpoint list) : Endpoint =
265+
subRouteWithExtensions (id) (path) (endpoints)
248266

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

270-
let addMetadata (metadata: obj) (endpoint: Endpoint) =
288+
let addMetadata (metadata: obj) (endpoint: Endpoint) : Endpoint =
271289
endpoint |> configureEndpoint _.WithMetadata(metadata)
272290

273291
// ---------------------------

src/Giraffe/Giraffe.fsproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<PropertyGroup>
33
<!-- General -->
44
<AssemblyName>Giraffe</AssemblyName>
5-
<AssemblyVersion>7.0.2</AssemblyVersion>
5+
<AssemblyVersion>8.0.0-alpha-001</AssemblyVersion>
66
<Description>A native functional ASP.NET Core web framework for F# developers.</Description>
77
<Copyright>Copyright 2020 Dustin Moris Gorski</Copyright>
88
<Authors>Dustin Moris Gorski and contributors</Authors>

0 commit comments

Comments
 (0)