Giraffe 1.1.0 - More routing handlers, better model binding and brand new model validation API
Published
Last week I announced the release of Giraffe 1.0.0, which (apart from some initial confusion around the transition to TaskBuilder.fs) went mostly smoothly. However, if you have thought that I would be chilling out much since then, then you'll probably be disappointed to hear that today I've released another version of Giraffe with more exciting features and minor bug fixes.
The release of Giraffe 1.1.0 is mainly focused around improving Giraffe's routing API, making model binding more functional and adding a new model validation API.
Some of these features address some long requested functionality, so let's not waste any more time and get straight down to it.
Routes with trailing slashes
Often I've been asked how to make Giraffe treat a route with a trailing slash equal to the same route without a trailing slash:
https://example.org/foo/bar
https://example.org/foo/bar/
According to the technical specification a route with a trailing slash is not the same as a route without it. A web server might want to serve a different response for each route and therefore Giraffe (rightfully) treats them differently.
However, it is not uncommon that a web application chooses to not distinguish between two routes with and without a trailing slash and as such it wasn't a surprise when I received multiple bug reports for Giraffe not doing this by default.
Before version 1.1.0 one would have had to specify two individual routes in order to make it work:
let webApp =
choose [
route "/foo" >=> text "Foo"
route "/foo/" >=> text "Foo"
]
Giraffe version 1.1.0 offers a new routing handler called routex
which is similar to route
except that it allows a user to specify Regex
in the route declaration.
This makes it possible to define routes with more complex rules such as allowing an optional trailing slash:
let webApp =
choose [
routex "/foo(/?)" >=> text "Foo"
]
The (/?)
regex pattern denotes that there can be exactly zero or one slash after /foo
.
With the help of routex
and routeCix
(the case insensitive version of routex
) one can explicitly allow trailing slashes (or other non-standard behaviour) in a single route declaration.
Parameterised sub routes
Another request which I have seen on several occasions was a parameterised version of the subRoute
http handler.
Up until Giraffe 1.0.0 there was only a routef
and a subRoute
http handler, but not a combination of both.
Imagine you have a localised application which requires a language parameter at the beginning of each route:
https://example.org/en-gb/foo
https://example.org/de-at/bar
etc.
In previous versions of Giraffe one could have used routef
to parse the parameter and pass it into another HttpHandler
function:
let fooHandler (lang : string) =
sprintf "You have chosen the language %s." lang
|> text
let webApp =
choose [
routef "/%s/foo" fooHandler
]
This was all good up until someone needed to make use of something like routeStartsWith
or subRoute
to introduce additional validation/authentication before invoking the localised routes:
let webApp =
choose [
// Doesn't require authentication
routef "/%s/foo" fooHandler
routef "/%s/bar" barHandler
// Requires authentication
requiresAuth >=> choose [
routef "/%s/user/%s/foo" userFooHandler
routef "/%s/user/%s/bar" userBarHandler
]
]
The problem with above code is that the routing pipeline will always check if a user is authenticated (and potentially return an error response) before even knowing if all subsequent routes require it.
The workaround was to move the authentication check into each of the individual handlers, namely the userFooHandler
and the userBarHandler
in this instance.
A more elegant way would have been to specify the authentication handler only one time before declaring all protected routes in a single group. Normally the subRoute
http handler would make this possible, but not if routes have parameterised arguments at the beginning of their paths.
The new subRoutef
http handler solves this issue now:
let webApp =
choose [
// Doesn't require authentication
routef "/%s/foo" fooHandler
routef "/%s/bar" barHandler
// Requires authentication
subRoutef "%s-%s/user" (
fun (lang, dialect) ->
// At this point it is already
// established that the path
// is a protected user route:
requiresAuth
>=> choose [
routef "/%s/foo" (userFooHandler lang dialect)
routef "/%s/bar" (userBarHandler lang dialect)
]
)
]
The subRoutef
http handler can pre-parse parts of a route and group a collection of cohesive routes in one go.
Improved model binding and model validation
The other big improvements in Giraffe 1.1.0 were all around model binding and model validation.
The best way to explain the new model binding and validation API is by looking at how Giraffe has done model binding in previous versions:
[<CLIMutable>]
type Adult =
{
FirstName : string
MiddleName : string option
LastName : string
Age : int
}
override this.ToString() =
sprintf "%s %s"
this.FirstName
this.LastName
member this.HasErrors() =
if this.Age < 18 then Some "Person must be an adult (age >= 18)."
else if this.Age > 150 then Some "Person must be a human being."
else None
module WebApp =
let personHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
let adult = ctx.BindQueryString<Adult>()
match adult.HasErrors() with
| Some msg -> RequestErrors.BAD_REQUEST msg
| None -> text (adult.ToString())
let webApp _ =
choose [
route "/person" >=> personHandler
RequestErrors.NOT_FOUND "Not found"
]
In this example we have a typical F# record type called Adult
. The Adult
type has an override for its ToString()
method to output something more meaningful than .NET's default and an additional member called HasErrors()
which checks if the provided data is correct according to the application's business rules (e.g. an adult must have an age of 18 or over).
There's a few problems with this implementation though. First you must know that the BindQueryString<'T>
extension method is a very loose model binding function, which means it will create an instance of type Adult
even if some of the mandatory fields (non optional parameters) were not present in the query string (or badly formatted). While this "optimistic" model binding approach has its own advantages, it is not very idiomatic to functional programming and requires additional null
checks in subsequent code.
Secondly the model validation has been baked into the personHandler
which is not a big problem at first, but means that there's a lot of boilerplate code to be written if an application has more than just one model to work with.
Giraffe 1.1.0 introduces new http handler functions which make model binding more functional. The new tryBindQuery<'T>
http handler is a stricter model binding function, which will only create an instance of type 'T
if all mandatory fields have been provided by the request's query string. It will also make sure that the provided data is in the correct format (e.g. a numeric value has been provided for an int
property of the model) before returning an object of type 'T
:
[<CLIMutable>]
type Adult =
{
FirstName : string
MiddleName : string option
LastName : string
Age : int
}
override this.ToString() =
sprintf "%s %s"
this.FirstName
this.LastName
member this.HasErrors() =
if this.Age < 18 then Some "Person must be an adult (age >= 18)."
else if this.Age > 150 then Some "Person must be a human being."
else None
module WebApp =
let adultHandler (adult : Adult) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
match adult.HasErrors() with
| Some msg -> RequestErrors.BAD_REQUEST msg
| None -> text (adult.ToString())
let parsingErrorHandler err = RequestErrors.BAD_REQUEST err
let webApp _ =
choose [
route "/person" >=> tryBindQuery<Adult> parsingErrorHandler None adultHandler
RequestErrors.NOT_FOUND "Not found"
]
The tryBindQuery<'T>
requires three parameters. The first is an error handling function of type string -> HttpHandler
which will get invoked when the model binding fails. The string
parameter in that function will hold the specific model parsing error message. The second parameter is an optional CultureInfo
object, which will get used to parse culture specific data such as DateTime
values or floating point numbers. The last parameter is a function of type 'T -> HttpHandler
, which will get invoked with the parsed model if model parsing was successful.
By using tryBindQuery<'T>
there is no danger of encountering a NullReferenceException
or the need of doing additional null
check any more. By the time the model has been passed into the adultHandler
it has been already validated against any data contract violations (e.g. all mandatory fields have been provided, etc.).
At this point the semantic validation of business rules is still embedded in the adultHandler
itself. The IModelValidation<'T>
interface can help to move this validation step closer to the model and make use of a more generic model validation function when composing the entire web application together:
[<CLIMutable>]
type Adult =
{
FirstName : string
MiddleName : string option
LastName : string
Age : int
}
override this.ToString() =
sprintf "%s %s"
this.FirstName
this.LastName
member this.HasErrors() =
if this.Age < 18 then Some "Person must be an adult (age >= 18)."
else if this.Age > 150 then Some "Person must be a human being."
else None
interface IModelValidation<Adult> with
member this.Validate() =
match this.HasErrors() with
| Some msg -> Error (RequestErrors.BAD_REQUEST msg)
| None -> Ok this
module WebApp =
let textHandler (x : obj) = text (x.ToString())
let parsingErrorHandler err = RequestErrors.BAD_REQUEST err
let tryBindQuery<'T> = tryBindQuery<'T> parsingErrorHandler None
let webApp _ =
choose [
route "/person" >=> tryBindQuery<Adult> (validateModel textHandler)
]
By implementing the IModelValidation<'T>
interface on the Adult
record type we can now make use of the validateModel
http handler when composing the /person
route. This functional composition allows us to entirely get rid of the adultHandler
and keep a clear separation of concerns.
First the tryBindQuery<Adult>
handler will parse the request's query string and create an instance of type Adult
. If the query string had badly formatted or missing data then the parsingErrorHandler
will be executed, which allows a user to specify a custom error response for data contract violations. If the model could be successfully parsed, then the validateModel
http handler will be invoked which will now validate the business rules of the model (by invoking the IModelValidation.Validate()
method). The user can specify a different error response for business rule violations when implementing the IModelValidation<'T>
interface. Lastly if the model validation succeeded then the textHandler
will be executed which will simply use the object's ToString()
method to return a HTTP 200
text response.
All functions are generic now so that adding more routes for other models is just a matter of implementing a new record types for each model and registering a single route in the web application's composition:
let webApp _ =
choose [
route "/adult" >=> tryBindQuery<Adult> (validateModel textHandler)
route "/child" >=> tryBindQuery<Child> (validateModel textHandler)
route "/dog" >=> tryBindQuery<Dog> (validateModel textHandler)
]
Overall the new model binding and model validation API aims at providing a more functional counter part to MVC's model validation, except that Giraffe prefers to use functions and interfaces instead of the System.ComponentModel.DataAnnotations
attributes. The benefit is that data attributes are often ignored by the rest of the code while a simple validation function can be used from outside Giraffe as well. F# also has the benefit of having a better type system than C#, which means that things like the [<Required>]
attribute have little use if there is already an Option<'T>
type.
Currently this new improved way of model binding in Giraffe only works for query strings and HTTP form payloads via the tryBindQuery<'T>
and tryBindFrom<'T>
http handler functions. Model binding functions for JSON and XML remain with the "optimistic" parsing model due to the underlying model binding libraries (JSON.NET and XmlSerializer
), but a future update with improvements for JSON and XML is planned as well.
In total you have the following new model binding http handlers at your disposal with Giraffe 1.1.0:
HttpHandler | Description |
---|---|
bindJson<'T> |
Traditional model binding. This is a new http handler equivalent of ctx.BindJsonAsync<'T> . |
bindXml<'T> |
Traditional model binding. This is a new http handler equivalent of ctx.BindAsync<'T> . |
bindForm<'T> |
Traditional model binding. This is a new http handler equivalent of ctx.BindFormAsync<'T> . |
tryBindForm<'T> |
New improved model binding. This is a new http handler equivalent of a new HttpContext extension method called ctx.TryBindFormAsync<'T> . |
bindQuery<'T> |
Traditional model binding. This is a new http handler equivalent of ctx.BindQueryString<'T> . |
tryBindQuery<'T> |
New improved model binding. This is a new http handler equivalent of a new HttpContext extension method called ctx.TryBindQueryString<'T> . |
bindModel<'T> |
Traditional model binding. This is a new http handler equivalent of ctx.BindModelAsync<'T> . |
The new model validation API works with any http handler which returns an object of type 'T
and is not limited to tryBindQuery<'T>
and tryBindFrom<'T>
only.
Roadmap overview
To round up this blog post I thought I'll quickly give you a brief overview of what I am planning to tackle next.
The next release of Giraffe is anticipated to be version 1.2.0 (no date set yet) which will mainly focus around improved authentication and authorization handlers (policy based auth support), better CORS support and hopefully better Anti-CSRF support.
After that if nothing else urgent comes up I shall be free to go over two bigger PRs in the Giraffe repository which aim at providing a Swagger integration API and a higher level API of working with web sockets in ASP.NET Core.