Skip to content

Commit 8478481

Browse files
committed
Added a bindQueryString function and extended bindModel to also look out for the query string
1 parent d093dc7 commit 8478481

File tree

5 files changed

+164
-21
lines changed

5 files changed

+164
-21
lines changed

README.md

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ The old NuGet package has been unlisted and will not receive any updates any mor
6666
- [bindJson](#bindjson)
6767
- [bindXml](#bindxml)
6868
- [bindForm](#bindform)
69+
- [bindQueryString](#bindquerystring)
6970
- [bindModel](#bindmodel)
7071
- [Installation](#installation)
7172
- [Sample applications](#sample-applications)
@@ -1054,9 +1055,64 @@ Accept: */*
10541055
Name=DB9&Make=Aston+Martin&Wheels=4&Built=2016-01-01
10551056
```
10561057

1058+
### bindQueryString
1059+
1060+
`bindQueryString<'T> (ctx : HttpHandlerContext)` can be used to bind a query string to a strongly typed model.
1061+
1062+
#### Example
1063+
1064+
Define an F# record type with the `CLIMutable` attribute which will add a parameterless constructor to the type:
1065+
1066+
```fsharp
1067+
[<CLIMutable>]
1068+
type Car =
1069+
{
1070+
Name : string
1071+
Make : string
1072+
Wheels : int
1073+
Built : DateTime
1074+
}
1075+
```
1076+
1077+
Then create a new `HttpHandler` which uses `bindQueryString` and use it from an app:
1078+
1079+
```fsharp
1080+
open Giraffe.HttpHandlers
1081+
open Giraffe.ModelBinding
1082+
1083+
let submitCar =
1084+
fun ctx ->
1085+
async {
1086+
// Binds a query string to a Car object
1087+
let! car = bindQueryString<Car> ctx
1088+
1089+
// Serializes the Car object back into JSON
1090+
// and sends it back as the response.
1091+
return! json car ctx
1092+
}
1093+
1094+
let webApp =
1095+
choose [
1096+
GET >=>
1097+
choose [
1098+
route "/" >=> text "index"
1099+
route "ping" >=> text "pong"
1100+
route "/car" >=> submitCar ]
1101+
```
1102+
1103+
You can test the bind function by sending a HTTP request with a query string:
1104+
1105+
```
1106+
GET http://localhost:5000/car?Name=Aston%20Martin&Make=DB9&Wheels=4&Built=1990-04-20 HTTP/1.1
1107+
Host: localhost:5000
1108+
Cache-Control: no-cache
1109+
Accept: */*
1110+
1111+
```
1112+
10571113
### bindModel
10581114

1059-
`bindModel<'T> (ctx : HttpHandlerContext)` can be used to automatically detect the `Content-Type` of a HTTP request and automatically bind a JSON, XML or form urlencoded payload to a strongly typed model.
1115+
`bindModel<'T> (ctx : HttpHandlerContext)` can be used to automatically detect the method and `Content-Type` of a HTTP request and automatically bind a JSON, XML,or form urlencoded payload or a query string to a strongly typed model.
10601116

10611117
#### Example
10621118

@@ -1096,7 +1152,9 @@ let webApp =
10961152
choose [
10971153
route "/" >=> text "index"
10981154
route "ping" >=> text "pong" ]
1099-
POST >=> route "/car" >=> submitCar ]
1155+
// Can accept GET and POST requests and
1156+
// bind a model from the payload or query string
1157+
route "/car" >=> submitCar ]
11001158
```
11011159

11021160
## Installation

samples/SampleApp/SampleApp/Program.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ let webApp =
101101
route "/once" >=> (time() |> text)
102102
route "/everytime" >=> warbler (fun _ -> (time() |> text))
103103
]
104-
POST >=> route "/car" >=> submitCar
104+
route "/car" >=> submitCar
105105
setStatusCode 404 >=> text "Not Found" ]
106106

107107
// ---------------------------------

src/Giraffe/Giraffe.fsproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<AssemblyName>Giraffe</AssemblyName>
5-
<VersionPrefix>0.1.0-alpha011</VersionPrefix>
5+
<VersionPrefix>0.1.0-alpha012</VersionPrefix>
66
<Description>A native functional ASP.NET Core web framework for F# developers.</Description>
77
<Copyright>Copyright 2017 Dustin Moris Gorski</Copyright>
88
<NeutralLanguage>en-GB</NeutralLanguage>

src/Giraffe/ModelBinding.fs

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ let readBodyFromRequest (ctx : HttpHandlerContext) =
1919
let bindJson<'T> (ctx : HttpHandlerContext) =
2020
async {
2121
let! body = readBodyFromRequest ctx
22-
let model = deserializeJson<'T> body
23-
return model
22+
return deserializeJson<'T> body
2423
}
2524

2625
let bindXml<'T> (ctx : HttpHandlerContext) =
@@ -34,7 +33,6 @@ let bindForm<'T> (ctx : HttpHandlerContext) =
3433
let! form = ctx.HttpContext.Request.ReadFormAsync() |> Async.AwaitTask
3534
let obj = Activator.CreateInstance<'T>()
3635
let props = obj.GetType().GetProperties(BindingFlags.Instance ||| BindingFlags.Public)
37-
3836
props
3937
|> Seq.iter (fun p ->
4038
let strValue = ref (StringValues())
@@ -43,21 +41,39 @@ let bindForm<'T> (ctx : HttpHandlerContext) =
4341
let converter = TypeDescriptor.GetConverter p.PropertyType
4442
let value = converter.ConvertFromString(strValue.Value.ToString())
4543
p.SetValue(obj, value, null))
44+
return obj
45+
}
4646

47+
let bindQueryString<'T> (ctx : HttpHandlerContext) =
48+
async {
49+
let query = ctx.HttpContext.Request.Query
50+
let obj = Activator.CreateInstance<'T>()
51+
let props = obj.GetType().GetProperties(BindingFlags.Instance ||| BindingFlags.Public)
52+
props
53+
|> Seq.iter (fun p ->
54+
let strValue = ref (StringValues())
55+
if query.TryGetValue(p.Name, strValue)
56+
then
57+
let converter = TypeDescriptor.GetConverter p.PropertyType
58+
let value = converter.ConvertFromString(strValue.Value.ToString())
59+
p.SetValue(obj, value, null))
4760
return obj
4861
}
4962

5063
let bindModel<'T> (ctx : HttpHandlerContext) =
5164
async {
52-
let original = ctx.HttpContext.Request.ContentType
53-
let parsed = ref (MediaTypeHeaderValue("*/*"))
65+
let method = ctx.HttpContext.Request.Method
5466
return!
55-
match MediaTypeHeaderValue.TryParse(original, parsed) with
56-
| false -> failwithf "Could not parse Content-Type HTTP header value '%s'" original
57-
| true ->
58-
match parsed.Value.MediaType with
59-
| "application/json" -> bindJson<'T> ctx
60-
| "application/xml" -> bindXml<'T> ctx
61-
| "application/x-www-form-urlencoded" -> bindForm<'T> ctx
62-
| _ -> failwithf "Cannot bind model from Content-Type '%s'" original
67+
if method.Equals "POST" || method.Equals "PUT" then
68+
let original = ctx.HttpContext.Request.ContentType
69+
let parsed = ref (MediaTypeHeaderValue("*/*"))
70+
match MediaTypeHeaderValue.TryParse(original, parsed) with
71+
| false -> failwithf "Could not parse Content-Type HTTP header value '%s'" original
72+
| true ->
73+
match parsed.Value.MediaType with
74+
| "application/json" -> bindJson<'T> ctx
75+
| "application/xml" -> bindXml<'T> ctx
76+
| "application/x-www-form-urlencoded" -> bindForm<'T> ctx
77+
| _ -> failwithf "Cannot bind model from Content-Type '%s'" original
78+
else bindQueryString<'T> ctx
6379
}

tests/Giraffe.Tests/ModelBindingTests.fs

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ open Xunit
88
open NSubstitute
99
open Microsoft.AspNetCore.Http
1010
open Microsoft.AspNetCore.Hosting
11+
open Microsoft.AspNetCore.Http.Internal
1112
open Microsoft.Extensions.Primitives
1213
open Microsoft.Extensions.Logging
1314
open Giraffe.Common
@@ -180,6 +181,40 @@ let ``bindForm test`` () =
180181
let body = getBody ctx
181182
Assert.Equal(expected, body)
182183

184+
[<Fact>]
185+
let ``bindQueryString test`` () =
186+
let ctx, hctx = initNewContext()
187+
188+
let queryHandler =
189+
fun ctx ->
190+
async {
191+
let! model = bindQueryString<Customer> ctx
192+
return! text (model.ToString()) ctx
193+
}
194+
195+
let app = GET >=> route "/query" >=> queryHandler
196+
197+
let queryStr = "?Name=John%20Doe&IsVip=true&BirthDate=1990-04-20&Balance=150000.5&LoyaltyPoints=137"
198+
let query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery queryStr
199+
let asdf = QueryCollection(query) :> IQueryCollection
200+
ctx.Request.Query.ReturnsForAnyArgs(QueryCollection(query) :> IQueryCollection) |> ignore
201+
ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore
202+
ctx.Request.Path.ReturnsForAnyArgs (PathString("/query")) |> ignore
203+
ctx.Response.Body <- new MemoryStream()
204+
205+
let expected = "Name: John Doe, IsVip: true, BirthDate: 1990-04-20, Balance: 150000.50, LoyaltyPoints: 137"
206+
207+
let result =
208+
hctx
209+
|> app
210+
|> Async.RunSynchronously
211+
212+
match result with
213+
| None -> assertFailf "Result was expected to be %s" expected
214+
| Some ctx ->
215+
let body = getBody ctx
216+
Assert.Equal(expected, body)
217+
183218
[<Fact>]
184219
let ``bindModel with JSON content returns correct result`` () =
185220
let ctx, hctx = initNewContext()
@@ -191,7 +226,7 @@ let ``bindModel with JSON content returns correct result`` () =
191226
return! text (model.ToString()) ctx
192227
}
193228

194-
let app = POST >=> route "/auto" >=> autoHandler
229+
let app = route "/auto" >=> autoHandler
195230

196231
let contentType = "application/json"
197232
let postContent = "{ \"Name\": \"John Doe\", \"IsVip\": true, \"BirthDate\": \"1990-04-20\", \"Balance\": 150000.5, \"LoyaltyPoints\": 137 }"
@@ -235,7 +270,7 @@ let ``bindModel with XML content returns correct result`` () =
235270
return! text (model.ToString()) ctx
236271
}
237272

238-
let app = POST >=> route "/auto" >=> autoHandler
273+
let app = route "/auto" >=> autoHandler
239274

240275
let contentType = "application/xml"
241276
let postContent = "<Customer><Name>John Doe</Name><IsVip>true</IsVip><BirthDate>1990-04-20</BirthDate><Balance>150000.5</Balance><LoyaltyPoints>137</LoyaltyPoints></Customer>"
@@ -279,7 +314,7 @@ let ``bindModel with FORM content returns correct result`` () =
279314
return! text (model.ToString()) ctx
280315
}
281316

282-
let app = POST >=> route "/auto" >=> autoHandler
317+
let app = route "/auto" >=> autoHandler
283318

284319
let contentType = "application/x-www-form-urlencoded"
285320
let headers = HeaderDictionary()
@@ -325,7 +360,7 @@ let ``bindModel with JSON content and a specific charset returns correct result`
325360
return! text (model.ToString()) ctx
326361
}
327362

328-
let app = POST >=> route "/auto" >=> autoHandler
363+
let app = route "/auto" >=> autoHandler
329364

330365
let contentType = "application/json; charset=utf-8"
331366
let postContent = "{ \"Name\": \"John Doe\", \"IsVip\": true, \"BirthDate\": \"1990-04-20\", \"Balance\": 150000.5, \"LoyaltyPoints\": 137 }"
@@ -347,6 +382,40 @@ let ``bindModel with JSON content and a specific charset returns correct result`
347382

348383
let expected = "Name: John Doe, IsVip: true, BirthDate: 1990-04-20, Balance: 150000.50, LoyaltyPoints: 137"
349384

385+
let result =
386+
hctx
387+
|> app
388+
|> Async.RunSynchronously
389+
390+
match result with
391+
| None -> assertFailf "Result was expected to be %s" expected
392+
| Some ctx ->
393+
let body = getBody ctx
394+
Assert.Equal(expected, body)
395+
396+
[<Fact>]
397+
let ``bindModel during HTTP GET request with query string returns correct result`` () =
398+
let ctx, hctx = initNewContext()
399+
400+
let autoHandler =
401+
fun ctx ->
402+
async {
403+
let! model = bindModel<Customer> ctx
404+
return! text (model.ToString()) ctx
405+
}
406+
407+
let app = route "/auto" >=> autoHandler
408+
409+
let queryStr = "?Name=John%20Doe&IsVip=true&BirthDate=1990-04-20&Balance=150000.5&LoyaltyPoints=137"
410+
let query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery queryStr
411+
let asdf = QueryCollection(query) :> IQueryCollection
412+
ctx.Request.Query.ReturnsForAnyArgs(QueryCollection(query) :> IQueryCollection) |> ignore
413+
ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore
414+
ctx.Request.Path.ReturnsForAnyArgs (PathString("/auto")) |> ignore
415+
ctx.Response.Body <- new MemoryStream()
416+
417+
let expected = "Name: John Doe, IsVip: true, BirthDate: 1990-04-20, Balance: 150000.50, LoyaltyPoints: 137"
418+
350419
let result =
351420
hctx
352421
|> app

0 commit comments

Comments
 (0)