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

Endpoint routing not working with .net 6 preview 6 minial api swagger integration #49

Open
albertwoo opened this issue Jul 20, 2021 · 35 comments
Assignees

Comments

@albertwoo
Copy link

I am trying falco with swagger. Because recently I read some post that swaggert now support .net 6 endpoint routing start from preview 6. But unfortuatly it is not working with falco endpoint.

Below is the demo code:

open System
open System.Threading.Tasks
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.DependencyInjection
open Falco
open Falco.Routing


let builder = WebApplication.CreateBuilder()

builder.Services.AddEndpointsApiExplorer()
builder.Services.AddHttpContextAccessor() |> ignore
builder.Services.AddSwaggerGen() |> ignore

let application = builder.Build()

application.UseSwagger() |> ignore


application.UseFalcoEndpoints [
    get "/" (Response.ofPlainText "Hello World")
]

// not working
// falco will call toRequestDelegate to convert handler into RequestDelegate which will erase the return type wrapped in Task
// if we can find a way to change to Func<_, _> style with more type info then we can integrate with swagger
application.MapGet("/hello1", RequestDelegate(fun (ctx: HttpContext) -> Task.FromResult $"Hi {ctx.Request.Path}" :> Task)) |> ignore

// working
application.MapGet("/hello2", Func<_, _>(fun (ctx: HttpContext) -> $"Hi {ctx.Request.Path}")) |> ignore

application.UseSwaggerUI() |> ignore

application.Run()
@pimbrouwers
Copy link
Owner

What happens when you use the webhost expression?

@albertwoo
Copy link
Author

It should be the same, because at first I tried that. And find it does not work so I switch to minial api style.

@baronfel
Copy link
Contributor

baronfel commented Jul 22, 2021

Related comment here that touches on why no F# library (Falco/Giraffe/etc) should expect the minimal API syntax to work for us out of the box. I had a conversation the other day where I touched on it in slightly more detail if you want to hear more as well.

@jkone27
Copy link

jkone27 commented Dec 21, 2021

Any news on this front? swashbuckle is still a missing integration (and painpoint) for all F# web frameworks?
has anyone yet tried to just "write" a custom openapi json object based on the registered endpoints, and send it to swashbuckle default endpoint for schema?
like
https://petstore.swagger.io/v2/swagger.json

can be served by serverside in F#

     get "v2/swagger.json" generateOpenapiSpecBasedOnRegisteredRoutes()

maybe using also this lib to generate it, or more simly json or yaml type providers with a rich default template file for the schema?

https://github.com/microsoft/OpenAPI.NET

?

@pimbrouwers
Copy link
Owner

I have literally no experience with Swashbuckle. I don't deliver formatted APIs professionally. So I am willing to assist anyone who wants to take the lead on this.

@jkone27
Copy link

jkone27 commented Dec 21, 2021

https://github.com/domaindrivendev/Swashbuckle.AspNetCore

i think with swashbuckle this would be sufficient (can also be pointed to a different custom endpoint if required),
and then we need to generate and serve the openapi.json content, to make the swashbuckle UI happy.

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("v2/swagger.json", "My API V2");
});

the not super easy part is how to get a list/description of registered endpoints with verbs, routes and models etc.

mainly we would have to generate the corresponding OpenApi spec file (e.g. see) https://editor.swagger.io/, based on registered endpoints/routes in falco (but would/could be nicer if it's general and can be re-used also in Giraffe and Saturn!

example:

https://petstore.swagger.io/
needs to have Openapi document served at
https://petstore.swagger.io/v2/swagger.json
to correctly generate the ui,that's all done by swashbuckle package (no work to do there),

we just need to generate the correct openapi spec

@jkone27
Copy link

jkone27 commented Dec 23, 2021

i think i found a way to "read endpoints" , at least in saturn if using endpoints routing (not sure if falco also already uses endpoints)

image

this is a start to dynamically create the openapi definition :)

@jkone27
Copy link

jkone27 commented Dec 23, 2021

there also seems to be this project, which is interesting
https://github.com/akhansari/FSharp.OpenApi

@pimbrouwers
Copy link
Owner

Nice work. Falco definitely uses endpoints. It even produces its own endpoint data source, as per the MSFT specs:

type internal FalcoEndpointDatasource(httpEndpoints : HttpEndpoint list) =

I'll have a closer look at all of this when I'm back from my holiday.

@jkone27
Copy link

jkone27 commented Dec 23, 2021

!! Thanks @pimbrouwers 👍 🎅 🎄 :) :) enjoy holidays

@pimbrouwers
Copy link
Owner

You as well!!

@baronfel
Copy link
Contributor

Getting the data source hasn't ever really been the problem here. The problem is in generating the OpenApi spec in a way that plugs in nicely with other ASP.NET Core middleware/libraries/etc. Currently, Swashbuckle builds the OpenAPI description from an ApiDescription, which ASP.NET Core builds itself via a Builder. This builder expects a certain shape of the data returned and so we'd need to either replicate that shape (hard) or provide alternate means of deriving these same shapes (might also be hard). Hope this helps.

@jkone27
Copy link

jkone27 commented Dec 23, 2021

and what also stucks me is getting type information.. as there i need somehow to get a Expression or quotation from a RequestDelegate ...

let getApiInformations (context: HttpContext) =
        
        let endpoints = context.GetService<IEnumerable<EndpointDataSource>>()
        
        let endpoint = endpoints |> Seq.head
        
        let endpointsInformation = 
            [ for m in endpoint.Endpoints.OfType<RouteEndpoint>() do
                let httpMethodMetadata = m.Metadata.[0] :?> HttpMethodMetadata
                let name = m.DisplayName //path HTTP: VERB
                //let arguments = m.RequestDelegate. TODO for Types
                yield { DisplayName = name; Metadata = httpMethodMetadata; }
            ]
            
        { Endpoints = endpointsInformation }

could only get this up to now ..

image

:D i know it probably looks funny to whom is experienced already 😀

probably the best way would be that the API library has a routing table with IN and OUT types etc..
that can be "queried" to build the openapi spec.

will def check that builder code 👍

@jkone27
Copy link

jkone27 commented Dec 23, 2021

it seems methodinfo has to be extracted from routeEndpoint.RequestDelegate in case of giraffe/saturn.

if only this could be invoked externally to not rely on MVC only code.. but it's private...

private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string httpMethod, MethodInfo methodInfo)
    {

@baronfel
Copy link
Contributor

yeah, that's why the built-in stuff doesn't work for any of the F# frameworks except those that use raw minimal APIs. the frameworks have to develop their own ways of reporting the metadata to the api description providers in order to plug in. it's a sad state of affairs, and will invariably lead to a lot of re-work :(

@jkone27
Copy link

jkone27 commented Jan 10, 2022

is there any way of ending up anywhere if we try re-write it completely without re-using the CreateApiDescription code from swashbuckle? for sure is quite many lines of code but on the other hand the "code is available", so maybe would be worth giving it a try for the F# community (else this whole openapi thing stays stuck forever as it has been for many years already?)

Suave is the only one having a working integration atm (as not relying on aspnetcore...)
https://rflechner.github.io/Suave.Swagger/

@jkone27
Copy link

jkone27 commented Jan 14, 2022

seems there is some work on from aspnet for NET7 milestone.. but it might take time..
dotnet/aspnetcore#37098

@jkone27
Copy link

jkone27 commented Apr 5, 2022

seems there was some progress on NET7 aspnet preview 2, maybe we could start trying it on a branch to see if it does work as expected :) https://devblogs.microsoft.com/dotnet/announcing-dotnet-7-preview-2/

@pimbrouwers
Copy link
Owner

pimbrouwers commented Aug 20, 2022

Apologies, I haven't been keeping a close enough eye on this.

I will create a branch for us.

@pimbrouwers pimbrouwers self-assigned this Aug 20, 2022
@zetashift
Copy link

Is there anything I can help with? It seems quite complicated, but generating an OpenAPI spec is a big part of getting people onboard with F# at $WORK for a new service

@essic
Copy link

essic commented Oct 11, 2024

Hello,

Any news on this subject please ?

@pimbrouwers
Copy link
Owner

Hi essic,

Can you kindly try the latest alpha release of version 5?

@jkone27
Copy link

jkone27 commented Oct 11, 2024

tried on script, annot get builder referenced.

generate aspnet scripts > dotnet fsi and then reference your dotnet runtime (i have 8.0.1 LTS)

image

maybe i am doing smt else wrong, but both Saturn and Giraffe work fine with the same approach
image

@pimbrouwers
Copy link
Owner

hey! nice to hear from you!

Version 5 doesn't have the host builder anymore. So, your code should be:

open Falco
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.Configuration

let bldr = WebApplication.CreateBuilder()
let wapp = WebApplication.Create()

let endpoints =
    [ get "/" (Response.ofPlainText "Hello World!") ]

wapp.UseFalco(endpoints)
    .Run()

@pimbrouwers
Copy link
Owner

pimbrouwers commented Oct 11, 2024

So I'm able to implement the IApiDescriptionProvider which consumes the Falco EndpointDataSource based off the code shared by @baronfel. But I'm not exactly sure how, or if we're able to, describe the parameters because they are lazily consumed in the HttpHandler itself. Unless I'm able to parse apart the route pattern to shim this.

Here is what I've got so far:

type internal FalcoApiDescriptionProvider (dataSource : FalcoEndpointDatasource) =
    let createApiDescription (endpoint : RouteEndpoint) httpMethod =
        let apiDescription = 
            ApiDescription(
                HttpMethod = httpMethod,
                RelativePath = endpoint.RoutePattern.RawText.TrimStart('/'),
                ActionDescriptor = ActionDescriptor(
                    DisplayName = endpoint.DisplayName))
        
        // TODO extract parameter info 
 
        apiDescription

    interface IApiDescriptionProvider with
        member _.Order = 0

        member _.OnProvidersExecuting(context: ApiDescriptionProviderContext) =
            for endpoint in dataSource.Endpoints do
                let httpMethodMetadata = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>()
                match endpoint, httpMethodMetadata with
                | :? RouteEndpoint as endpoint, httpMethod when not(isNull httpMethod) ->
                    for httpMethod in httpMethodMetadata.HttpMethods do
                        context.Results.Add(createApiDescription endpoint httpMethod)
                | _ -> 
                    ()

        member _.OnProvidersExecuted(context: ApiDescriptionProviderContext) =
            ()

@pimbrouwers
Copy link
Owner

pimbrouwers commented Oct 14, 2024

I'm so excited and relieved to share that I cracked the nut on this one. It was painful to say the least, and I need to polish it off, but the meat and pertaters is there.

What the end user will likely see:

open Falco
open Falco.OpenApi
open Falco.Routing
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting

let endpoints =
    [
        get "/" (Response.ofPlainText "Hello World!")
        |> OpenApi.name "TestName"
        |> OpenApi.description "This is a test"
        |> OpenApi.returnType typeof<string>
        // or, more explicitly
        // |> OpenApi.returns { Return = typeof<string>; ContentTypes = [ "text/plain" ]; Status = 200 }
    ]

    let bldr = WebApplication.CreateBuilder(args)

    bldr.Services
        .AddFalcoOpenApi()
        .AddSwaggerGen()
        |> ignore

    let wapp = bldr.Build()

    wapp.UseHttpsRedirection()
        .UseSwagger()
        .UseSwaggerUI()
    |> ignore

    wapp.UseFalco(endpoints)
    |> ignore

    wapp.Run()
    0

My thinking is that it will be delivered as a separate nuget package. The source code is here, if you'd like to follow along: https://github.com/pimbrouwers/Falco.OpenApi/tree/master/src/Falco.OpenApi

I'll roll this out as soon as I can for more official testing. Open to any initial thoughts.

@jkone27
Copy link

jkone27 commented Oct 15, 2024

This is great 📰 news ! thank you 😊. A nice way to add http integration tests on this one could be with Hawaii and/or SwaggerProvider on some sample project 👏

@pimbrouwers
Copy link
Owner

@jkone27 I'll look into that. Thank you! Do you like the semantics of it?

@essic
Copy link

essic commented Oct 15, 2024

Hi essic,

Can you kindly try the latest alpha release of version 5?

I will, thanks 👍🏿

@pimbrouwers
Copy link
Owner

pimbrouwers commented Oct 17, 2024

I believe that I've completed a reasonable alpha version of this. There were ultimately two files in the aspnetcore repository that provided most useful in determining how the OpenAPI document service interacts with the metadata of aspnetcore.

Really happy with being able to add this, and how the public API shaped up. I'm really looking forward to all of your feedback.

I just posted an alpha3 version of Falco, with the required updates to make OpenAPI support work. Sometime tomorrow I'll post an alpha1 of Falco.OpenAPI for everyone to test.

Thanks for your input!

--

An example:

namespace OpenApi

open Falco
open Falco.OpenApi
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting

type FortuneInput =
    { Name : string }

type Fortune =
    { Description : string }
    static member Create age input =
        match age with
        | Some age when age > 0 ->
            { Description = $"{input.Name}, you will experience great success when you are {age + 3}." }
        | _ ->
            { Description = $"{input.Name}, your future is unclear." }

type Greeting =
    { Message : string }

module Program =
    open Falco.Routing

    let endpoints =
        [
            mapPost "/fortune"
                (fun r -> r?age.AsIntOption())
                (fun ageOpt ->
                    Request.mapJson<FortuneInput> (Option.defaultValue { Name = "Joe" }
                    >> Fortune.Create ageOpt
                    >> Response.ofJson))
                |> OpenApi.name "Fortune"
                |> OpenApi.summary "A mystic fortune teller"
                |> OpenApi.description "Get a glimpse into your future, if you dare."
                |> OpenApi.query [
                    { Type = typeof<int>; Name = "Age"; Required = false } ]
                |> OpenApi.acceptsType typeof<FortuneInput>
                |> OpenApi.returnType typeof<Fortune>

            mapGet "/hello/{name?}"
                (fun route ->
                    { Message = route?name.AsString("world") })
                Response.ofJson
                |> OpenApi.name "Greeting"
                |> OpenApi.description "A friendly greeter"
                |> OpenApi.route [
                    { Type = typeof<string>; Name = "Name"; Required = false } ]
                |> OpenApi.query [
                    { Type = typeof<int>; Name = "Age"; Required = false } ]
                |> OpenApi.acceptsType typeof<string>
                |> OpenApi.returnType typeof<Greeting>

            get "/" (Response.ofPlainText "Hello World!")
                |> OpenApi.name "HelloWorld"
                |> OpenApi.description "This is a test"
                |> OpenApi.returnType typeof<string>
        ]

    [<EntryPoint>]
    let main args =
        let bldr = WebApplication.CreateBuilder(args)

        bldr.Services
            .AddFalcoOpenApi()
            .AddSwaggerGen()
            |> ignore

        let wapp = bldr.Build()

        wapp.UseHttpsRedirection()
            .UseSwagger()
            .UseSwaggerUI()
        |> ignore

        wapp.UseFalco(endpoints)
        |> ignore

        wapp.Run()
        0

@pimbrouwers
Copy link
Owner

The alpha1 package is available for those interested in testing. Thank you in advance for your feedback!

https://www.nuget.org/packages/Falco.OpenApi/1.0.0-alpha1

@jkone27
Copy link

jkone27 commented Oct 18, 2024

as a suggestion, would it possible to configure openapi also within falco CEs? i do not see it in version 5-alpha, what would be the most succint way of adding this in a semi-transparent way for users (supposing most of times user want openapi, should be a relatively small "flag" or portion of code to enable it)


webHost [||] {
    endpoints [
        get "/" 
            ("Hello World" |> Response.ofPlainText)
        |> OpenApi.name "HelloWorld"
        |> OpenApi.description "returns hello world"
        |> OpenApi.returnType typeof<string>
    ]
}
 |> OpenApi.withSwagger // ?

or is the plan to abandon webHost in Falco 5.x ? if so also fine ofc

@jkone27
Copy link

jkone27 commented Oct 18, 2024

thanks! 🤩 working falco-openapi.fsx for the fsi fans, you need to create your runtime script using IcedTask generate-sdk-references.fsx in your local folder before running this one

#nowarn "20"
#load "runtime-scripts/Microsoft.AspNetCore.App-latest-8.fsx"
#r "nuget: Falco, 5.0.0-alpha3"
#r "nuget: Falco.OpenApi, 1.0.0-alpha1"
#r "nuget: Swashbuckle.AspNetCore"

open Falco
open Falco.OpenApi
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Hosting
open Falco.Routing
open Swashbuckle

let endpoints = 
    [
        get "/" 
            ("Hello World" |> Response.ofPlainText)
        |> OpenApi.name "HelloWorld"
        |> OpenApi.description "returns hello world"
        |> OpenApi.returnType typeof<string>
    ]

let configure (builder: WebApplicationBuilder) =
    builder.Services
        .AddFalcoOpenApi()
        .AddSwaggerGen()
    builder.Build()

let configureApp (app: WebApplication) =
    app.UseRouting()
        .UseSwagger() // for generating OpenApi spec
        .UseSwaggerUI() // for viewing Swagger UI
        .UseFalco(endpoints)
    app

let webApp = 
    WebApplication.CreateBuilder()
    |> configure
    |> configureApp

webApp.Run()

@pimbrouwers
Copy link
Owner

pimbrouwers commented Oct 18, 2024

as a suggestion, would it possible to configure openapi also within falco CEs? i do not see it in version 5-alpha, what would be the most succint way of adding this in a semi-transparent way for users (supposing most of times user want openapi, should be a relatively small "flag" or portion of code to enable it)


webHost [||] {
    endpoints [
        get "/" 
            ("Hello World" |> Response.ofPlainText)
        |> OpenApi.name "HelloWorld"
        |> OpenApi.description "returns hello world"
        |> OpenApi.returnType typeof<string>
    ]
}
 |> OpenApi.withSwagger // ?

or is the plan to abandon webHost in Falco 5.x ? if so also fine ofc

The goal of v5 and beyond is stay out of the web server side of it. There are naturally certain extension methods that are required. But beyond that, I'll let the very smart people at Microsoft do their good work there.

I always felt this pressure to make the server and configuration construction more elegant in F# (the ignores get a little obtrusive). But I've found that a couple of simple extension methods allow for better chaining of service and middleware registration. I've included a few I've found helpful in the WebApplication module.

All of examples will incorporate what people need to know and how I like to utilize everything (as a suggestion). They'll all be part of the docs now, with discrete explanations of the interesting parts. Truth be told, that's been the bulk of the work in v5, documentation. Otherwise, it's most resection and the further unification of request data (with additional support for collections over the wire).

@essic
Copy link

essic commented Oct 24, 2024

For what it's worth, I believe that it's a very pragmatic decision, it's less elegant but support should be easier. Even though I am going to miss the old way.

Overall I think that v5 is going to be dope !

It took me some time since I got caught up in other stuff but I finally got to try and it works !

Screenshot 2024-10-24 at 22 42 44 Screenshot 2024-10-24 at 22 44 29
let wapp2 =
    WebApplication
        .CreateBuilder()
        .AddServices(fun cfg svc ->
            let githubPat = cfg.GetValue("GithubPat")

            svc.AddFalcoOpenApi().AddSwaggerGen() |> ignore

            let mvcCoreSvc =
                svc
                    .Configure(cfg)
                    .AddHttpClient()
                    .AddMvcCore()
                    .AddJsonOptions(fun opts ->
                        opts.JsonSerializerOptions.PropertyNamingPolicy <- JsonNamingPolicy.SnakeCaseLower)

            let configureProblemDetailsService (opts: ProblemDetailsOptions) =
                opts.IncludeExceptionDetails <- (fun ctx _ -> true) // for now
                opts.MapToStatusCode<Exception>(500)
                //We make sure we stay in lower snake case
                opts.ExceptionDetailsPropertyName <- "exception_details"
                opts.TraceIdPropertyName <- "trace_id"

            mvcCoreSvc.Services
                .AddSingleton<IStoreStuff, FakeMemoryStore>()
                .AddSingleton<IProvideTheTime, TimeProvider>()
                .AddProblemDetails(configureProblemDetailsService)
                .AddGithubService("mvp-platform-api", githubPat))
        .Build()

let endpoints =
    [ get "/operation/all" Handlers.Operations.Global.listOperations
      post "/operation/create-repository" Handlers.Operations.Repository.createRepository
      get "/operation/status/id/{OperationId}" Handlers.Operations.Repository.getRepositoryCreationStatus ]

(wapp2
     .UseProblemDetails()
     .UseSwagger()
     .UseSwaggerUI()
     .UseFalco(endpoints) :?> WebApplication)
    .Run()

If there's some things you'd like me to do to test things, feel free.
Thanks !

PS: don't mind the API, it's going to be RESTify when needed 😛

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants