giraffe-fsharp / Giraffe

A native functional ASP.NET Core web framework for F# developers.
https://giraffe.wiki
Apache License 2.0
2.12k stars 266 forks source link

Endpoint router and composition #453

Open toburger opened 3 years ago

toburger commented 3 years ago

I am using a HOF pattern that allows me to reuse a big load of logic with HttpHandler.

For example I am using the following function to extract the Environment from the DI service and have it usable on the wrapping HttpHandler:

/// Retrieve environment and make it available to the sub handler
let withEnvironment (handler: Environment -> HttpHandler): HttpHandler =
    fun next ctx -> task {
        let env = ctx.GetService<Environment>()
        return! handler env next ctx
    }

/// Extracts query string CC0 and if provided show all images, otherwise filter away non CC0 images
let extractFilter (handler: string[] -> HttpHandler): HttpHandler =
    fun next ctx -> task {
        match ctx.GetQueryStringValue "CC0" with
        | Ok _ -> return! handler [||] next ctx
        | Error _ -> return! handler nonCC0Filter next ctx
    }
let galleryHandler filter: HttpHandler =
    withEnvironment (fun env ->
        // Now I can use the Environment values
        let config = env.Config
        choose [
            route "/galleries" >=> galleriesHandler filter
            routef "/galleries/%s" (galleryHandler filter)
            routef "/galleries/%s/%s" (galleryBrowserHandler [config.GalleryPath])
            routef "/thumbs/%s/%s" (galleryBrowserHandler [config.ThumbsPath; config.GalleryPath])
            ...
       ]
}

let filteredGalleryHandler: HttpHandler =
     extractFilter galleryHandler

This doesn't seem to be possible with endpoint routing, so I wonder how I could solve such a use case.

The only way I was able to solve the problem was to inverse the call chain but this leads to a substantial code duplication:

let galleryHandler = [
    route "/galleries" (extractFilter galleriesHandler)
    routef "/galleries/%s" (fun gallery -> extractFilter (fun filter -> galleryHandler filter gallery))
    routef "/gallery/%s/%s" (fun (gallery, image) ->
        extractFilter (fun filter ->
            withEnvironment (fun env ->
                let config = env.Config
                galleryBrowserHandler [config.GalleryPath] (gallery, image))))
    routef "/thumbs/%s/%s" (fun (gallery, image) ->
        extractFilter (fun filter ->
            withEnvironment (fun env ->
                let config = env.Config
                galleryBrowserHandler [config.ThumbsPath; config.GalleryPath] (gallery, image))))
     ...
]
dustinmoris commented 3 years ago

You are correct that this is not possible with endpoint routing anymore. I am not sure if there is any clever trick we could apply to make it possible again, because your extractFilter handler requires access to the HttpContext and QueryString which is not available at the time of registering an endpoint. The Endpoint Routing Middleware by ASP.NET Core will resolve an endpoint at runtime and only then invoke a handler with the given HttpContext.

toburger commented 3 years ago

Your commit has closed the wrong issue... :wink:

I see!

I wonder if my trick is "too clever" and there is a better pattern to archive my goal. On the other hand, the nice thing about HttpHandler is that it is so wonderfully composable so that it allows the above pattern. The endpoint routing on the other hand has the benefit to register metadata to the request which can be used to generate OpenAPI information (right?) and is the new hotness in ASP.NET Core :wink:.

So we have basically three choices: 1) OOTB: composability 2) TokenRouter: composability and routing performance 3) Endpoint Routing: routing performance and metadata

dustinmoris commented 3 years ago

Your commit has closed the wrong issue...

Ops, yes, sorry about that!

So we have basically three choices: OOTB: composability TokenRouter: composability and routing performance Endpoint Routing: routing performance and metadata

I am afraid your summary is spot on.