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

Support attributes on lambda expressions #984

Open
5 tasks done
cartermp opened this issue Feb 24, 2021 · 40 comments
Open
5 tasks done

Support attributes on lambda expressions #984

cartermp opened this issue Feb 24, 2021 · 40 comments

Comments

@cartermp
Copy link
Member

cartermp commented Feb 24, 2021

I propose we support attributes on lambda expressions. A way this could get used is in the experimental aspnet Houdini project, which is experimenting ways to cut the cruft in aspnet core service development, including an alternate routing system than MVC. One of their approaches would be mapping endpoints with lambdas:

record Todo(int Id, string Name, bool IsComplete);

app.MapAction([HttpPost("/")] ([FromBody] Todo todo) : Todo => todo);
app.MapAction([HttpGet("/")] () : Todo => new(Id: 0, Name: "Play more!", IsComplete: false));

Note that the lambda itself has an atttribute. In F#, the above snippet would probably look something like this:

type Todo = { Id: int; Name: string; IsComplete: bool }

app.MapAction([<HttpPost("/")>] fun ([<FromBody>] todo) -> todo) |> ignore
app.MapAction([<HttpGet("/")>] fun () -> { Id = 0; Name = "Play more!"; IsComplete = false }) |> ignore

The existing way of approaching this problem in F#, using the above example, would be to pull out the lambda bodies into separate functions and pass them into a constructed Func:

type Todo = { Id: int; Name: string; IsComplete: bool }

[<HttpPost("/")>]
let echoTodo ([<FromBody>] todo) = todo

[<HttpGet("/")>]
let getTodo () = { Id = 0; Name = "Play more!"; IsComplete = false }

app.MapAction(Func<Todo,Todo>(echoTodo)) |> ignore
app.MapAction(Func<Todo>(getTodo)) |> ignore

This isn't really a bad alternative though, the main thing is that it's annoying to have to construct a func. Instead, lobbying to get MapAction to support equivalent FSharpFunc types as overloads and simply have those overloads construct the appropriate Func type could make this approach nice from an F# perspective.

However, I still think it's worth considering (even if people decide it's not really useful for F#) since this is an approach to programming against an API that more library authors may take in the future.

Pros and Cons

The advantages of making this adjustment to F# are:

  • Orthogonality I suppose, since you can decorate an F# function with an attribute. If you do this with a function and then want it to also work with a lambda, you're fine. Although lambdas aren't 100% in correspondence with a function - see use of byrefs for more information.
  • Works nicely with APIs that expect people to use this sort of pattern

The disadvantages of making this adjustment to F# are:

  • Allows for "call site complication" with lots of information stuffed into a lambda, making seemingly succinct code somewhat complex / difficult to understand
  • Encourages stuffing lots of functionality into a lambda expression. Although this is somewhat common, especially in longer pipelines where you don't have a need to pull out an expression into its own function since it's only used once, it's still not "typical" F# code.
  • Encourages a way to interact with APIs that we may not want or need
  • Not terribly discoverable

Extra information

Estimated cost (XS, S, M, L, XL, XXL): M

Related suggestions: (put links to related suggestions here)

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I or my company would be willing to help implement and/or test this

For Readers

If you would like to see this issue implemented, please click the 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.

@charlesroddie
Copy link

charlesroddie commented Feb 24, 2021

ways to cut the cruft in aspnet core service development

So near and yet so far.

A lot of object-oriented programming styles are so averse to using objects.
I wonder why they do not go all the way and replace all type information with annotation?

[<HttpGet("/")>]
[<Id(0)>]
[<Name<"Play more!">>]
[<IsComplete(false)>]
let todo = object()

Then everything is object()!

Overall MapAction seems uninteresting to F# because it's intrinsically annotation-based and has weird JSON dependencies which imply that everything is implicit not explicit. F# should not support this sort of thing. Intstead there should be an action creator with a genuine type signature, such that when given objects of the right types the code will work.

@Happypig375
Copy link
Contributor

We can use this to support static lambdas, if we decide to do it.

@halter73
Copy link

halter73 commented Feb 25, 2021

We're planning to make the route pattern an explicit parameter again rather than take it from an attribute which should hopefully reduce "call site complication". dotnet/aspnetcore#30448

With this change, the F# example would look more like the following:

type Todo = { Id: int; Name: string; IsComplete: bool }

app.MapPost("/", fun ([<FromBody>] todo) -> todo) |> ignore
app.MapGet("/", fun () -> { Id = 0; Name = "Play more!"; IsComplete = false }) |> ignore

@charlesroddie
Copy link

charlesroddie commented Feb 25, 2021

@halter73 An API that respects the .Net type system would avoid nongeneric classes like Delegate, annotations like [<FromBody>]. It would be something like:

MapPost(string pattern, System.Func<System.Web.HttpRequest, System.Web.Mvc.ActionResult> poster)
MapGet(string pattern, System.Func<System.Web.Mvc.ActionResult> getter)

@davidfowl
Copy link

Then we'd need infinity (hyberbole) overloads. Which makes things even more complex (any number of arguments, sync vs async, ValueTask vs Task)

@charlesroddie
Copy link

Then we'd need infinity (hyberbole) overloads. Which makes things even more complex (any number of arguments, sync vs async, ValueTask vs Task)

Sync/ValueTask/Task is 3. Why would you need "any number of arguments"?

@halter73
Copy link

halter73 commented Mar 5, 2021

Why would you need "any number of arguments"?

For convenient parameter binding. So instead of having let! todo = httpRequest.ReadFromJsonAsync<Todo>() |> Async.AwaitTask, the framework can do this for you before calling a user-defined Action<Todo> or Func<Todo, Task<MyResponse>> or whatever.

If a developer also wants to get an id from the route (ex: "/api/todos/{id}"), they can add an int parameter instead of calling let id = int httpRequest.RouteValues.["id"] and having to somehow deal with invalid ints. By adding an int parameter named id, you declaratively tell the framework that you expect the {id} part of the route to be an int and to not even call into user code if it's not.

@davidfowl
Copy link

Sync/ValueTask/Task is 3. Why would you need "any number of arguments"?

We can even natively support F# async without defining that overload.

Because we want to support binding arguments from various sources:

  • Route
  • QueryString
  • Header
  • Cookie
  • Request Body
  • Multipart Form
  • Services

The beauty of supporting any function type is that we can add support for new things in the future, without adding more overloads.

@davidfowl
Copy link

@cartermp It occurs to me that this is just one of the many features we need to make this work smoothly on F#. The other is around supporting conversion from F# lambdas to Delegate without an explicit cast.

@dustinmoris
Copy link

dustinmoris commented Mar 26, 2021

So thanks to @davidfowl I'm going to leave some feedback here, but unfortunately I have many random thoughts which are not strictly limited to this specific issue. I appreciate that this is not the right place to log them all, but if it's ok with you I'll dumb my thoughts here for now and then move any actionable issue elsewhere with your guidance.

Ok, so here we go...

First I welcome the changes in ASP.NET Core to introduce a more flexible API which feels closer to the metal. At least this is what my impression is by looking at what you guys are working on and I think it's a great idea!

Personally I think the current approach is not bold enough. It's just another "big" framework with a slightly different syntax. IMHO the approach taken tries to solve too many responsibilities at once. The new Houdini style framework tries to appease to too many developers, people like myself who want to have simple to use and flexible lower level API as well as to the "spoilt" developer who just wants a "plug & play" MVC-style API. I think trying to achieve both with a single framework will ultimately fail to deliver the right abstraction which can appeal to both groups and will only change the perception temporarily at best.

If it was my decision then I'd suggest to create a very low level (almost Go like) HTTP API which is super flexible and truly bare bones which gives developers maximum control and yet a good enough convenience API which can empower the minimalist developer to feel extremely productive and not having to fight the tools. Using those low level APIs Microsoft could create a second higher level "plug & play" framework for the easy beginner friendly path. Currently this was meant to be ASP.NET Core and MVC, but I think they failed because the lines were not clearly drawn and many ASP.NET Core APIs were specifically built with MVC in mind and not thought of as a standalone library. I almost think that both these things should get built by different teams to avoid the same mistakes and not mud the waters.

Now taking a bit more specifically about the low level API which I'd like to see. I don't think it needs (or should) have attributes at all. Declarative APIs are very high level. It means there is something else sitting below which expects certain idioms to be followed in order to carry out useful tasks. If something doesn't follow these idioms then things stop working or fall apart. At least things will not work entirely as advertised which is what will users give the feeling of fighting the tools. Regardless how well one thinks they have designed those idioms, you can only design what you anticipate for. Developers will always want to do something new, something unconventional, something which seemed silly one day but makes sense today and then a declarative API will quickly show its cracks.

Therefore I suggest to keep it truly low. No attributes, no declarative model binding. Just have low level functions such as

  • ctx.Request.TryBindModel<T>
  • ctx.Response.WriteAsync(bytes, ct)
  • ctx.Response.SetHeader("Content-Type", "application/json")
  • etc.

Does it mean that things like authentication through a single middleware might not work or that another middleware won't be able to produce comprehensive Swagger docs? Yes, but that is ok. I would even suggest that concerns such as authentication and CORS should be moved closer to the endpoints where they are actually being required and not sit in a global middleware which therefore relies on a heavy declarative API in order to centrally control every single endpoint.

Again, this only has to be true for the low level API. Move things closer to where it is required. Having something like app.MapAction("/foo", requiresBasicAuth(cors((someHandler))) would be really nice.

Yes it's ugly, but it's low level beauty. People who will use that will build their own meaningful wrappers and APIs to build the app which they want. It empowers them and gives them 100% control and lets them create the beautiful APIs which they see fit.

On top of these simple idioms you can have a separate attribute-driven API which gives you a more out of the box experience. Of course, in return this will force a user down a certain path but it's a trade-off that people will accept when they know that they can break out into a lower level world if they know that exists.

The other thing which I would try to keep in mind is really the naming of things and how it affects the beauty of APIs. @davidfowl mentioned it on Twitter and I couldn't agree more. Beauty is important and in my opinion a big principle of beauty is to ensure that the name of a method or function must match the complexity underneath, otherwise people will think it's magic or too verbose.

For example, mapping a single string to a route feels like a very trivial task. A method name like MapAction doesn't strike me as a great name as it's unnecessarily long and verbose:

app.MapAction([HttpPost("/")] ([FromBody] Todo todo) : Todo => todo);

Better:

app.GET("/", ctx => someFunctionToHandle)

However, if something is more complex then it is ok to have a slightly longer name.

Bad:

var model = ctx.Bind<T>()

Good:

var model = ctx.Request.Body.Bind<T>()

Now coming back to F#....

Overall I think it would be a mistake to introduce Attributes as a core concept into F#. We already have them, but their use is very limited and often only in relation to C# interop, but such a change would make attributes a first class citizen which feels wrong if it's only needed in order to mould F# around a new .NET web framework which couldn't let go of a declarative API.

EDIT:

Until ASP.NET Core offers true low level APIs which don't need attributes or where endpoints must rely on centrally controlled authentication/CORS/etc middlewares then developers will always come and label the current framework as too heavy and too bloated because if they reject the existing API they are left with very little to no other options. Please believe me, I can guarantee you 100% that this will be the case because it's always been like that.

@davidfowl
Copy link

That's really good feedback, feel free to close this issue. I see now that this isn't idiomatic at all in F# abs users are better served by something like giraffe. We'll definitely consider adding something like bind (though we do that already have some bring for JSON, and it's not clear that we need any more than that).

As for the requireAuth(cors(...)) I'm not sure we'd add something like that since we already have an imperative way of adding metadata to endpoints.

Again, this is good feedback, muchly appreciated

@cartermp
Copy link
Member Author

@davidfowl

The other is around supporting conversion from F# lambdas to Delegate without an explicit cast.

F# supports direct conversions at the call site like so:

open System
type C() =
    member _.M(f: Func<int, int>) = ()
    member _.M(f: Action<int>) = ()
    
let c = C()

c.M(fun x -> x + 1)  // direct func interop
c.M(fun x -> printfn $"{x}") // action interop

let funcThing = fun x -> x + 1
let actionThing = fun x -> printfn $"{x}"

c.M(funcThing)  // direct func interop
c.M(actionThing) // action interop

But there can be some gaps, and it'd probably ease current interop patterns with ASP.NET Core. In the preview bits I played with for some reason I couldn't just interop directly like I can above, so that could be addressed separately.

As for the general point @dustinmoris and @davidfowl I don't think there's much harm in F# adopting smoother .NET interop support for existing constructs like this. It is also a matter of orthogonality that is worth discussing - if you can put an attribute on an F# function, why not a lambda when they have near-identical correspondence? I think there are certainly some drawbacks to using it, and I would personally prefer not to use an API that requires lots of attributes. But that's also why we have wonderful choices like Giraffe, Saturn, Falco, Suave, etc.

@davidfowl perhaps a better way to ease F# use of aspnetcore is to work out a few of the pain points with @dustinmoris and identify things where:

  • aspnetcore could make a small addition for F# libraries that sit atop the primitives to work smoother (e.g., ease configuration or use of application settings)
  • F# language could improve interop (e.g., what it takes to ensure no explicit Func or Action construction is done at a call site for MapAction)

I don't have context on the former piece, but I'd see it as akin to MVC supporting routes that are written directly as F# async. When that was done, MVC apps didn't need to call Async.StartAsTask at the end of each route if they wanted to keep using F# async.

@dsyme
Copy link
Collaborator

dsyme commented Mar 26, 2021

But there can be some gaps, and it'd probably ease current interop patterns with ASP.NET Core. In the preview bits I played with for some reason I couldn't just interop directly like I can above, so that could be addressed separately.

FWIW these issues are, I believe, ironed out by https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1093-additional-conversions.md. However we should trial and check (it may not apply to overloaded cases for example).

I don't think there's much harm in F# adopting smoother .NET interop support for existing constructs like this. It is also a matter of orthogonality that is worth discussing - if you can put an attribute on an F# function, why not a lambda when they have near-identical correspondence?

I agree we should address this as it's a basic matter of interop-capability for F# and we'll inevitably encounter attribute-aware .NET frameworks

@halter73
Copy link

halter73 commented Mar 27, 2021

The other is around supporting conversion from F# lambdas to Delegate without an explicit cast.

I guess this got lost when this got turned into an issue. Here's the original email for context.

Hi Phillip,

I'm reaching out because we're planning to add support for lambda attributes in C# 10 and I'm wondering if this also makes sense for F#. It's going to be important for a MapAction() API we're planning to add to ASP.NET.

Here's a small example of using MapAction with and without the new lambda features in C#:

Comparing main..lambda-attributes-return · halter73/HoudiniPlayground (github.com)

And the comparison in F#:

Comparing f1b94d2...b70aa73 · halter73/HoudiniPlaygroundFSharp (github.com)

Aside from supporting attributes on lambdas and lambda arguments, this also relies on lambdas having a "natural type" when assigned to a "Delegate" (the type expected by MapAction). We want to avoid an error like the following:

C:\dev\halter73\HoudiniPlaygroundFSharp\Startup.fs(24,36): error FS0193: Type constraint mismatch. The type ↔ 'unit -> int' ↔is not compatible with type↔ 'Delegate'

Even trying to downcast with :?> Func<Todo> fails with the following, so the "before" in my F# before and after sample doesn't even compile today.

C:\dev\halter73\HoudiniPlaygroundFSharp\Startup.fs(31,37): error FS0016: The type 'unit -> Todo' does not have any proper subtypes and cannot be used as the source of a type test or runtime coercion.

Do you think this is something F# could support? I think an API like MapAction lends itself to interactive programming of the type F# seeks to enable. In fact, I've been using .NET Interactive for a lot of testing. It's been with C# so far, but I'd like to be able to use F#!

@cartermp Then contributed some PRs to the HoudiniPlaygroundFSharp repo that's gotten it closer to something that could theoretically work: halter73/HoudiniPlaygroundFSharp#1 and halter73/HoudiniPlaygroundFSharp#2, but neither version works yet the MapPost doesn't work yet because ASP.NET cannot see the [<FromBody>] attribute on todo.

Edit: The MapGet does work because there aren't any attributes required.

@davidfowl
Copy link

Right I'd like the examples that @halter73 posted to work with F# as well, attributes aside

@halter73
Copy link

halter73 commented Mar 27, 2021

We have a new design at dotnet/aspnetcore#30248 that will infer the sources for some parameters without attributes that follow conventions common to ASP.NET Core MVC.

Because of these conventions, even the [<FromBody>] attribute on the POST can now be removed leaving no attribute left in the original sample!! Attributes will still be important to get the full flexibility of the API.

After the changes, the following will work. The MapGet already works today if you use the nightlies.

type Todo = { Id: int; Name: string; IsComplete: bool }

app.MapPost("/", Func<Todo,Todo>(fun todo -> todo)) |> ignore
app.MapGet("/", Func<Todo>(fun () -> { Id = 0; Name = "Play more!"; IsComplete = false })) |> ignore

Having to write out Func<Todo,Todo> feels a lot like the unnecessary casting we currently have to do in C#, but since that allows for type inference in the lambda definition maybe it's not so bad in F#.

@charlesroddie
Copy link

charlesroddie commented Mar 27, 2021

@dustinmoris If it was my decision then I'd suggest to create a very low level (almost Go like) HTTP API which is super flexible and truly bare bones with maximum control but good enough in terms of convenience so that power users can be productive without fighting the tools and then a second plug&play framework on top of that low level API for the easy beginner friendly path.

YES. And the low level API should be a type-safe one. The convenience API can be completely designed with typical C# devs in mind.

@davidfowl and @halter73 were suggesting that for convenience we should lose static typing and use a dynamic convention-based system.

  • @davidfowl mentioned "binding arguments from various sources", but F# does not have a problem with expressing logic and works well with optional types so extracting data from sources isn't a problem. TBH it's pretty straightforward in C# too.

  • @halter73 gave a more difficult example "/api/todos/{id}" where something extra tells the system that id is an int. Let's assume this is "/api/todos/{INT id}". Here there is the fundamental type and then convenience. The fundamental type takes the path-like string and returns a Func<HttpRequest, ActionResult>?, which if null means that the route is not followed at all. It's straightforward to write this directly if you don't mind being explicit about parsing. For a convenience layer, that will be language specific, because different languages want different things and because the current code-generation strategy is different across languages. C# can define source generators or use your dynamic idea. F# could define a type provider: Get<"/api/todos/{INT id}">.(fun (httpReq, pathArgs) -> pathArgs.Id ... ) // pathArgs has an int Id property.

The ASP.Net team shouldn't need to bother about a convenience layer for F# since we should be able to use a type-safe layer directly or write our own convenience layer. The important thing is that all of this can be built on top of a type-safe core API. What is not acceptable is forcing people to use a dynamic API in .Net! Any dynamic API should be optional (nuget package or linker-compatible so it can get linked out if not used).

@davidfowl
Copy link

Thanks @charlesroddie. We don't want to force anyone to use anything and want to expose the right building blocks so other libraries can build idiomatic layers. We already have a strongly typed core built around a strongly typed RequestDelegate (giraffe is built on top of it). We've also added APIs to read and write JSON from and to the request and response and will continue to do so as we push more features into the core as building blocks.

That doesn't change what we're asking for here, but I think it's partially up to the F# community to decide if this is anti F# or not. The building blocks already exist and we will continue to build more of them but we also like the declarative model for C# as it can remove lots of boilerplate in lots of cases. This isn't zero sum.

@toburger
Copy link

toburger commented Mar 27, 2021

That doesn't change what we're asking for here, but I think it's partially up to the F# community to decide if this is anti F# or not. The building blocks already exist and we will continue to build more of them but we also like the declarative model for C# as it can remove lots of boilerplate in lots of cases. This isn't zero sum.

I think that's the crucial point here:
When speaking of declarative programming in C# often an AOP approach is meant (applying attributes to parameters to control application specific semantics) that weakens the type checking (everything comes together at runtime).
In F# declarative programming is achieved by using F#'s idioms such as expressions (instead of statements), EDSLs and type safe programming which composes at the type checker level.

@dustinmoris
Copy link

dustinmoris commented Mar 27, 2021

app.MapPost("/", Func<Todo,Todo>(fun todo -> todo)) |> ignore

I don't know what type app is here, but if there was a Router object I'd love to use an API like this:

let router = new Router()
router.GET("/", indexHandler)

...
// During web host creation:
app.Endpoints.MapRouter(router)

app.Run()

Also the |> ignore is extremely annoying in F#. That means that your current MapGet methods return an object to do chained mapping probably like MapGet(....).MapPost(...). I wonder if that is really required? Is this how YOU see the API should be used? If not and if all examples map one route per statement then why not just make it a void method and then F# developers don't have to ignore everything? I'm just asking because it seems that in C# you actually don't really envision it to be chained so if you change the return type to match your own expectations then you'd make a huge difference to F# developers as well! The other alternative would be to have an overload or another set of the same methods just voided so that it's more natural from F#.

EDIT:

That's really good feedback, feel free to close this issue.

@davidfowl I didn't mean to put a downer on the Houdini API or suggest that this issue should get closed. Clearly the ASP.NET team sees a desire for a web framework which appeals more to the minimalist developer and I only wanted to share some thoughts of someone who considers themselves a minimalist developer with what I thought could be a problem. I think the developers who want to be productive with a higher level declarative API are already very well served with MVC and I think the new Houdini API doesn't necessarily have to compete with MVC but maybe could focus more on what MVC doesn't do well today.

I don't know how much is already set in stone, but I'm happy to provide as much constructive feedback as you think is still helpful at this point :)

@halter73
Copy link

halter73 commented Mar 27, 2021

The app is an IEndpointRouteBuilder. The returned object isn't for chained mapping like MapGet(....).MapPost(...). It's for adding metadata to the route for things like auth.

app.MapPost("/", Func<Todo,Todo>(fun todo -> todo)).RequireAuthorization() |> ignore

I agree the |> ignore is extremely annoying though.

If you're interested the docs are at https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-5.0#routing-concepts

@dustinmoris
Copy link

dustinmoris commented Mar 27, 2021

Right, I see....

Another option which I think could work nicely for C# and F# could be by making the handler method the final void part of the chain, forcing metadata to be set beforehand:

 app.POST("/api/foo").RequiresAuth().EnableCors().Handle(fun todo -> todo)

However, the problem only exists because your current framework requires those extra metadata functions in order to feed the information into a central auth middleware and other middlewares. If The API was "lighter" then none of that would even be an issue to begin with :)

@dustinmoris
Copy link

dustinmoris commented Mar 27, 2021

Also, re naming, see how my example reads so much simpler and quicker and allows a developer to comprehend what is happening than the version where everything is stretched much longer with MapAction/MapPost, RequireAuthorization, ...

app.MapPost("/", Func<Todo,Todo>(fun todo -> todo)).RequireAuthorization() |> ignore

vs

app.POST("/").RequiresAuth().Handle<Func<Todo,Todo>>(fun todo -> todo)

The benefit of a final Handle chain function could also solve the type casting like so:

type TodoHandler = Func<Todo,Todo>
app.POST("/").RequiresAuth().Handle<TodoHandler>(fun todo -> todo)

@halter73
Copy link

halter73 commented Mar 27, 2021

Personally, I think void Handle method at the end of the chain reads well and like that it gets rid of the |> ignore. We already ship MapGet/MapPost/... RequestDelegate overloads with the metadata chained at the end though, so it doesn't really make sense to switch conventions at this point. I do think libraries like giraffe can help here by building more idiomatic F# APIs on top of it and similar APIs like RequestDelegateFactory (dotnet/aspnetcore#31181).

@dustinmoris
Copy link

dustinmoris commented Mar 27, 2021

Also one more thing, I'm not even sure if RequiresAuth() is a good abstraction, because that means it will lead to something like this:

type TodoHandler = Func<Todo,Todo>
app.POST("/").RequiresAuth(fun options ->
    // configure auth options
).Handle<TodoHandler>(fun todo -> todo)

Which leads to chains of builders within builders which are really not nice. They are not nice in production code and they are not nice in demos. Many layers of builders make C#/F# code look like a snake with a lot of indentation going in and out multiple times, a bit like a huge HTML file with tons of nested divs. Just the raw look of it makes things look "huge" and "bloated" and "complicated". This kinds of feeds back into the beauty of APIs again. The more the final code is just a set of simple top level statements the nicer it feels and reads. Just my own opinion.

I'd suggest to stick with the first level of abstraction and just make better use of that like so:

type TodoHandler = Func<Todo,Todo>
app.POST("/").BasicAuth().JWTAuth().SessionCookies().Handle<TodoHandler>(fun todo -> todo)

@dustinmoris
Copy link

@halter73

Sure I understand that certain APIs have already been shipped and are set in stone, but if there is still an opportunity to make things nice across all of .NET then it's worth considering.

After all Giraffe is not a different web framework, I never saw it that anyway. It's just a wrapper library to make ASP.NET Core work for F#. Honestly, I wish this wasn't required in the first place if you see what I mean.

@halter73
Copy link

halter73 commented Mar 27, 2021

I completely agree we should make ASP.NET Core as usable out of the box as possible for F#. I appreciate the input. There's still a lot of open design space for the API, but the other existing overloads for MapGet/MapPost/... do create expectations.

Another API you might be interested in is our new "direct hosting" API dotnet/aspnetcore#30354. It's heavily inspired by Feather HTTP.

@dustinmoris
Copy link

Cool will take a look. Thanks for linking!

@davidfowl
Copy link

davidfowl commented Mar 27, 2021

That's good feedback about some of the fluent APIs and how it has the potential to complicate the code and make it look like a snake. In practice that doesn't happen because options aren't specified the way you describe.

Also one big design goal is NOT to change the existing idioms we're already created. We're not building a new framework, we're refining what we have carefully so we don't break the existing patterns and can take advantage of all the code that is and the ecosystem has written over the last 5 years. That is an explicit design goal.

@davidfowl
Copy link

PS: I also like the handle method at the end of the fluent chain of calls. We can propose an alternative API for building up a route, that'd be interesting.

@lambdakris
Copy link

lambdakris commented Mar 29, 2021

I still program mostly in C# for work and I gotta admit that I really like the look of something like what @dustinmoris suggested, for both F# and C#!

app.Post("/").RequiresAuth().Handle((Todo todo) => 
{ 
  return todo;
});

So I like the idea of looking into an alternative API for building up a route, for C# as much as for F# :)!

P.S. I also support the idea of adding support for Attributes on lambda expressions in F#, if for nothing else than completeness/symmetry

@Happypig375
Copy link
Contributor

@lambdakris That's a weird hybrid of C# and F# there

@smoothdeveloper
Copy link
Contributor

@Happypig375 the kind of weird hybrid that @cartermp code fixers will fix in F# editor 😄

@lucasteles
Copy link

Despite the minimal-api applicability, I feel like the attributes on lambda expressions is something F# should have, in the end, will be an interop limitation, if not with minimal-api, with other libraries or frameworks of .NET ecosystem

@T-Gro
Copy link

T-Gro commented Jan 2, 2023

I have tried searching for documentation on this, but failed - how does a typical C# library code search for attributes on arguments of lambdas? Ideally following a doc recommending how these things are typically written.
Ideally in an example of real C# code, that does the basic checks (alongside of checking attributes on classes, methods and their arguments).

Why I ask this:
The F# codegen can put an attribute on the generated method behind the F# lambda, but that still gives no guarantee that C# written library code will find the attribute where it expects to. (e.g. if designed around Func, I could imagine it might expect to see the argument placed on the generated delegateArg0 here https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AbEAzAzgHwxgBcACAcxIDFcALAQygAcAZegW2ABN6AKASlIBeUr2wBXAHakAnqTgA+WaQDUpAKz8AsACgiZPA2ZtOPYRWp1GrDtz7a9JUvVwBBKlLDmAyjNzEYdgA6D0kwAB4AS0liNGjiJTErY1sefiA=).

@CosminSontu
Copy link

Any chance this will make its way into F# 8 ?
Thanks

@vzarytovskii
Copy link

Any chance this will make its way into F# 8 ? Thanks

No, it needs a formal approval, as well as F# 8 is frozen feature-wise.

@En3Tho
Copy link

En3Tho commented Oct 13, 2023

Wanted to share that there is a really cool library for cli based apps which has minimal api inspired design:
https://github.com/Cysharp/ConsoleAppFramework

And example is the topic is not working.

type Todo = { Id: int; Name: string; IsComplete: bool }

[<HttpPost("/")>]
let echoTodo ([<FromBody>] todo) = todo

[<HttpGet("/")>]
let getTodo () = { Id = 0; Name = "Play more!"; IsComplete = false }

let oneMoreTest(webApp: WebApplication) =
    webApp.Map("", Func<Todo,Todo>(echoTodo)) |> ignore
    webApp.Map("", Func<Todo>(getTodo)) |> ignore

results in (decompiled):

public static void oneMoreTest(WebApplication webApp)
    {
      RouteHandlerBuilder routeHandlerBuilder = webApp.Map("", (Delegate) new Func<WebApplicationTests.Todo, WebApplicationTests.Todo>(WebApplicationTests.oneMoreTest@49.Invoke));
      routeHandlerBuilder = webApp.Map("", (Delegate) new Func<WebApplicationTests.Todo>(WebApplicationTests.oneMoreTest@50-1.Invoke));
    }

[CompilationMapping(SourceConstructFlags.Closure)]
    [Serializable]
    [SpecialName]
    [StructLayout(LayoutKind.Auto, CharSet = CharSet.Auto)]
    internal static class oneMoreTest@49
    {
      internal static WebApplicationTests.Todo Invoke(WebApplicationTests.Todo delegateArg0) => delegateArg0;
    }

    [CompilationMapping(SourceConstructFlags.Closure)]
    [Serializable]
    [SpecialName]
    [StructLayout(LayoutKind.Auto, CharSet = CharSet.Auto)]
    internal static class oneMoreTest@50-1
    {
      internal static WebApplicationTests.Todo Invoke() => new WebApplicationTests.Todo(0, "Play more!", false);
    }

As you see Func's are created from syntesized type which does not even propagate the attribute. Is is simply lost. Parameter names are lost too.

@jkone27
Copy link

jkone27 commented Mar 4, 2024

This is going to be in F# 9?

@vzarytovskii
Copy link

This is going to be in F# 9?

This hasn't been approved, so not likely.

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

No branches or pull requests