-
Notifications
You must be signed in to change notification settings - Fork 266
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
Functional binding of querystrings #121
Comments
Hey, that looks quite cool. I'll have a closer look at the code, but I can see how it's useful and think about a possible integration! |
Hi @Alxandr , I was trying to use your snippet, but I have some problem after copy/paste your code: Any suggestions? |
As far as I can see, your issue is indentation. The operators needs to be indented more than [Edit] |
@Alxandr I have discovered that
It appears that @Alxandr 's
I also really like this approach (ie. Chiron's approach: https://github.com/xyncro/chiron) because it eliminates the need for reflection, favoring instead an explicit deserialization interface that is decoupled from the type's CLI representation. |
This does not look nice/simple to me, you have had to state each property name 3x, one time being loosely by string that is not verified and susceptible to run time error, as well as importing the whole Chiron library and using funky operators. Rather then bloat the base with this it might be worth trying to review underlying issues first:
@jonashw thanks for providing the following feedback, offers us a starting point:
It is not too difficult to expand Naturally users free to use Chiron approach but I would rather see |
I've gone ahead and added support for Lists/Sets in my own custom version of type QueryStringValueType =
| OptionType of Type
| List of Type
| SetType of Type
| Other
type HttpContext with
member this.TryGetQueryValue (key : string) =
let q = this.Request.Query
match q.TryGetValue key with
| true, value ->
Some value
| _ -> None
member this.BindQueryStringJW<'T>(?cultureInfo : CultureInfo) =
let obj = Activator.CreateInstance<'T>()
let culture = defaultArg cultureInfo CultureInfo.InvariantCulture
let props = obj.GetType().GetProperties(BindingFlags.Instance ||| BindingFlags.Public)
props
|> Seq.iter (fun p ->
let queryValueType =
if not (p.PropertyType.GetTypeInfo().IsGenericType)
then Other
else
let genericType = p.PropertyType.GetGenericTypeDefinition()
let genericArg = p.PropertyType.GetGenericArguments().[0]
if genericType = typedefof<Option<_>>
then OptionType genericArg
elif genericType = typedefof<List<_>>
then List genericArg
elif genericType = typedefof<Set<_>>
then SetType genericArg
else Other
let propertyType =
let pt =
match queryValueType with
| Other -> p.PropertyType
| List t -> t
| OptionType t -> t
| SetType t -> t
if pt.GetTypeInfo().IsValueType
then (typedefof<Nullable<_>>).MakeGenericType([|pt|])
else pt
let converter = TypeDescriptor.GetConverter propertyType
let value =
match queryValueType, (this.TryGetQueryValue p.Name) with
| Other, None -> null
| Other, Some v -> converter.ConvertFromString(null, culture, v.ToString())
| List _, None ->
FSharpValue.MakeUnion(FSharpType.GetUnionCases(p.PropertyType).[0], [||])
| List _, Some v ->
let consCase, empty =
let cases = FSharpType.GetUnionCases(p.PropertyType)
cases.[1], FSharpValue.MakeUnion(cases.[0], [||])
if v.Count = 0
then empty
else Array.foldBack
(fun item list -> FSharpValue.MakeUnion(consCase, [|item :> obj ; list|]))
(v.ToArray())
empty
| SetType genericType, None ->
let listType = (typedefof<List<_>>).MakeGenericType(genericType)
let emptyList = FSharpValue.MakeUnion(FSharpType.GetUnionCases(listType).[0], [||])
Activator.CreateInstance(p.PropertyType, [|emptyList|])
| SetType _, Some v ->
let values = v.ToArray() |> Array.toList
Activator.CreateInstance(p.PropertyType, values)
| OptionType _, None ->
FSharpValue.MakeUnion(FSharpType.GetUnionCases(p.PropertyType).[0], [||])
| OptionType _, Some v ->
let cases = FSharpType.GetUnionCases(p.PropertyType)
let v = converter.ConvertFromString(null, culture, v.ToString())
if isNull v
then FSharpValue.MakeUnion(cases.[0], [||])
else FSharpValue.MakeUnion(cases.[1], [|v|])
p.SetValue(obj, value, null))
obj |
Note that the above does not address the let value =
match queryValueType, (this.TryGetQueryValue p.Name) with
| Other, None -> null
... |
@gerardtoconnor You don't have to like it, nor do you have to use it. I posted this here because some people might like it, and it fits well with the functional paradigm of programming web services (in my opinion). But since it was just a single file, I didn't want to make it a library that nobody ever found. But to respond to a bit of your feedback:
This is sort of true, but also not necessary. If you have a function of type
This is the case for all query parsing. Even if you base your query names from property names (using reflection, or any other way), there is no strong coupling that what is sent from the client actually matches with what your model expects. You get no added safety by using reflection for this.
Chiron is not a dependency. You don't import it at all.
You don't have to use a single "funky" operator. For instance, you can write it like this instead: type Person =
{ name: string
age: int }
static member FromQuery (_: Person) =
query {
let! name = Query.read "name"
let! age = Query.read "age"
return { name = name; age = age } } @whitebear-gh the best I can guess based on the little information I have to go off is that you're lacking |
@Alxandr thanks a lot! |
So I was reading all the feedback from this issue and I am working on improving I was thinking to do the following:
Behaviour of new "strict" model binding of query string:
*) For now it will be able to bind simple union types: // Will work:
type PaymentMethod =
| Credit
| Debit
| Cash
// Will not work:
type PaymentMethod =
| Credit of CreditCard
| Debit of DebitCard
| Cash I have two questions for everyone involved here:
Thanks everyone for the help and patience! |
I would still use my helper, because I really don't like the imperative API of |
@Alxandr Thanks for the reply! What you say regarding the Need to think a bit more about it... I don't think I understand what you mean by "don't like the imperative API of BindQueryString" though? What is imperative about calling a function which will map a string value (= query string) to a record type? This is not much different than other F# APIs like different serializers, the FSharp.Data API, etc., right? |
Hmm. Imperative might have been the wrong name. What I mainly don't like is the fact that you need to have the let reportRoute =
route "/report"
>=> Query.bind (fun (r: Report) -> text <| sprintf "%A" r) |
Also; you can bake validation into the types themselves. The static |
Ah ok I see... yeah that is quite nice... hmm thanks for helping me understand the issue better! Since you seem to be very responsive right now could you quickly provide me an example how a client would pass a list in a query string? I am trying to address this as well:
Thank you for the help so far! |
There are multiple ways to pass lists using query strings. And arguments on which ones are the best 😛 . As far as I know MVC uses Ways:
|
Something to consider regarding lists/arrays in query strings is compatibility with frontend libraries. For example, many JavaScript libraries follow the first convention
|
Awesome, thank you for all the information! |
Hi, I have implemented an improved model binding API based on the feedback provided here. There is a new The new implementation continues to use reflection as I prefer that model over having to do it manually in a member method of the record type. I've also thought about how to allow a user to specify additional business logic validation and added a new model validation API. Overall I think this addresses all issues raised in this ticket. A run through of the new features can be found in this blog post. Nevertheless I have added a reference to @Alxandr Chrion based implementation in the documentation (see at the bottom of this section) in case users prefer to implement their own query string binding functions. Also in terms of binding lists from a query string I just used ASP.NET Core's default implementation, which supports both, the |
I needed some querystring data in a small app I made to try out Giraffe, and the
ctx.BindQuery
wasn't doing it for me; so I made a functional query string binder based on how Chiron works which allows me to do the following:I was thinking this might be something you'd want to incorporate into Giraffe? Anyways, if people want it, the code is available here: https://gist.github.com/Alxandr/50aef7fbe4806ceb4c2889f1cbde1438
Feel free to use it however you'd like :)
The text was updated successfully, but these errors were encountered: