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

Add new route functions (...WithExtensions) + .NET 8 and 9 #634

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
]
},
"fsharp-analyzers": {
"version": "0.26.0",
"version": "0.28.0",
"commands": [
"fsharp-analyzers"
]
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.x
7.x
8.x
9.x
- name: Restore
run: dotnet restore
- name: Build
Expand All @@ -48,7 +47,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.x
dotnet-version: 9.x
- name: Restore tools
run: dotnet tool restore
- name: Build solution
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/fantomas-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ concurrency:
jobs:
fantomas-check:
name: Code format check
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down
6 changes: 2 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,8 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.x
7.x
8.x
9.x
- name: Restore
run: dotnet restore
- name: Build
Expand All @@ -54,9 +53,8 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.x
7.x
8.x
9.x
- name: Create Release NuGet package
run: |
arrTag=(${GITHUB_REF//\// })
Expand Down
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 in the short run.

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`. For now, since this is in alpha, Giraffe is only accepting extensions with this signature:

```fsharp
[<RequireQualifiedAccess>]
type AspNetExtension =
| RateLimiting of string
| CacheOutput of string
```

And you can use it like this:

```fsharp
let endpoints: list<Endpoint> =
[
GET [
routeWithExtensions "/rate-limit" [ AspNetExtension.RateLimiting MY_RATE_LIMITER ] (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. 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.

If there are other extensions you'd like to use, feel free to submit your patches through GitHub PRs, or by creating new issues to let us know.

### 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", "{A38653BD-95EE-4AA4-9D89-EB26C672BF4F}"
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
{A38653BD-95EE-4AA4-9D89-EB26C672BF4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A38653BD-95EE-4AA4-9D89-EB26C672BF4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A38653BD-95EE-4AA4-9D89-EB26C672BF4F}.Debug|x64.ActiveCfg = Debug|Any CPU
{A38653BD-95EE-4AA4-9D89-EB26C672BF4F}.Debug|x64.Build.0 = Debug|Any CPU
{A38653BD-95EE-4AA4-9D89-EB26C672BF4F}.Debug|x86.ActiveCfg = Debug|Any CPU
{A38653BD-95EE-4AA4-9D89-EB26C672BF4F}.Debug|x86.Build.0 = Debug|Any CPU
{A38653BD-95EE-4AA4-9D89-EB26C672BF4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A38653BD-95EE-4AA4-9D89-EB26C672BF4F}.Release|Any CPU.Build.0 = Release|Any CPU
{A38653BD-95EE-4AA4-9D89-EB26C672BF4F}.Release|x64.ActiveCfg = Release|Any CPU
{A38653BD-95EE-4AA4-9D89-EB26C672BF4F}.Release|x64.Build.0 = Release|Any CPU
{A38653BD-95EE-4AA4-9D89-EB26C672BF4F}.Release|x86.ActiveCfg = Release|Any CPU
{A38653BD-95EE-4AA4-9D89-EB26C672BF4F}.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}
{A38653BD-95EE-4AA4-9D89-EB26C672BF4F} = {9E6451FB-26E0-4AE4-A469-847F9602E999}
EndGlobalSection
EndGlobal
11 changes: 11 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Release Notes
=============

## 8.0.0 - 202x-xx-xx

With this release, we're deprecating support for .NET 6 and 7. Other than that, we're starting to support .NET 9. Some packages were updated due to this SDK pump.

- [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
- [[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
60 changes: 60 additions & 0 deletions samples/RateLimiting/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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 "/rate-limit" [ AspNetExtension.RateLimiting MY_RATE_LIMITER ] (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>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading
Loading