An in depth functional reference to all of Giraffe's default features.
- Fundamentals
- Basics
- Web Request Processing
- Giraffe View Engine
- Serialization
- Testing
- Miscellaneous
- Additional Features
- Special Mentions
- Appendix
The main building block in Giraffe is a so called HttpHandler
:
type HttpFuncResult = Task<HttpContext option>
type HttpFunc = HttpContext -> HttpFuncResult
type HttpHandler = HttpFunc -> HttpContext -> HttpFuncResult
an HttpHandler
is a function which takes two curried arguments, an HttpFunc
and an HttpContext
, and returns an HttpContext
(wrapped in an option
and Task
workflow) when finished.
On a high level an HttpHandler
function receives and returns an ASP.NET Core HttpContext
object, which means every HttpHandler
function has full control of the incoming HttpRequest
and the resulting HttpResponse
.
Each HttpHandler
can process an incoming HttpRequest
before passing it further down the Giraffe pipeline by invoking the next HttpFunc
or short circuit the execution by returning an option of Some HttpContext
.
If an HttpHandler
doesn't want to process an incoming HttpRequest
at all, then it can return None
instead. In this case a surrounding HttpHandler
might pick up the incoming HttpRequest
or the Giraffe middleware will defer the request to the next RequestDelegate
from the ASP.NET Core pipeline.
The easiest way to get your head around a Giraffe HttpHandler
is to think of it as a functional equivalent to the ASP.NET Core middleware. Each handler has the full HttpContext
at its disposal and can decide whether it wants to return Some HttpContext
, None
or pass it on to the "next" HttpFunc
.
The Giraffe pipeline is a (sort of) functional equivalent of the (object oriented) ASP.NET Core pipeline. The ASP.NET Core pipeline is defined by nested middleware and the Giraffe pipeline is defined by HttpHandler
functions. The Giraffe pipeline is plugged into the wider ASP.NET Core pipeline through the GiraffeMiddleware
itself and therefore an addition to it rather than a replacement.
If the Giraffe pipeline didn't process an incoming HttpRequest
(because the final result was None
and not Some HttpContext
) then other ASP.NET Core middleware can still process the request (e.g. static file middleware or another web framework plugged in after Giraffe).
This architecture allows F# developers to build rich web applications through a functional composition of HttpHandler
functions while at the same time benefiting from the wider ASP.NET Core eco system by making use of already existing ASP.NET Core middleware.
The compose
combinator combines two HttpHandler
functions into one.
It is the main combinator in Giraffe which allows composing many smaller HttpHandler
functions into a bigger web application:
let app = compose (route "/") (Successful.OK "Hello World")
A slightly more convenient and more commonly used form of compose
is the fish operator >=>
:
let app = route "/" >=> Successful.OK "Hello World"
There is no limit to how many HttpHandler
functions can be chained with compose
or the fish operator:
let app =
route "/"
>=> setHttpHeader "X-Foo" "Bar"
>=> setStatusCode 200
>=> setBodyFromString "Hello World"
If you would like to learn more about the >=>
(fish) operator then please check out Scott Wlaschin's blog post on Railway oriented programming.
The choose
combinator function iterates through a list of HttpHandler
functions and invokes each individual handler until the first HttpHandler
returns a positive result:
let app =
choose [
route "/foo" >=> text "Foo"
route "/bar" >=> text "Bar"
]
If your route is not returning a static response, then you should wrap your function with a warbler
:
// ('a -> 'a -> 'b) -> 'a -> 'b
let warbler f a = f a a
Functions in F# are eagerly evaluated and a normal route will only be evaluated the first time. A warbler will ensure that a function will get evaluated every time the route is hit:
// unit -> string
let time() = System.DateTime.Now.ToString()
let webApp =
choose [
route "/normal" >=> text (time())
route "/warbler" >=> warbler (fun _ -> text (time()))
]
Another important aspect of Giraffe is that it natively works with .NET's Task
and Task<'T>
objects instead of relying on F#'s historic async {}
workflows. The main benefit of this is that it removes the necessity of converting back and forth between tasks and async workflows when building a Giraffe web application (because ASP.NET Core only works with tasks out of the box).
Giraffe 6 targets .NET 6 and uses F# 6's built-in task support without any additional dependencies.
When building web apps using Giraffe, we recommend you use this built-in support too.
In Giraffe 5, we use the task {}
computation expression from the Ply NuGet package. Syntactically it works identical to F#'s async workflows (after opening the FSharp.Control.Tasks
module):
open Giraffe
let personHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let! person = ctx.BindModelAsync<Person>()
return! json person next ctx
}
The task {}
CE is an independent project maintained by Crowded, for more information please visit the official Ply GitHub repository.
IMPORTANT NOTICE
If you have do!
bindings in your Giraffe 5 web application then you must open the FSharp.Control.Tasks
namespace to resolve any type inference issues:
open FSharp.Control.Tasks
There's multiple ways how one can create a new HttpHandler
in Giraffe.
The easiest way is to re-use an existing HttpHandler
function:
let sayHelloWorld : HttpHandler = text "Hello World, from Giraffe"
You can also add additional parameters before returning an existing HttpHandler
function:
let sayHelloWorld (name : string) : HttpHandler =
let greeting = sprintf "Hello World, from %s" name
text greeting
If you need to access the HttpContext
object then you'll have to explicitly return an HttpHandler
function which accepts an HttpFunc
and HttpContext
object and returns an HttpFuncResult
:
let sayHelloWorld : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
let name =
ctx.TryGetQueryStringValue "name"
|> Option.defaultValue "Giraffe"
let greeting = sprintf "Hello World, from %s" name
text greeting next ctx
Because an HttpHandler
is defined as HttpFunc -> HttpContext -> HttpFuncResult
you will need to apply the next
and ctx
parameters to the subsequent handler (in this case text
).
The most verbose version of defining a new HttpHandler
function is by explicitly returning a Task<HttpContext option>
. This is useful when an async operation needs to be called from within an HttpHandler
function:
type Person = { Name : string }
let sayHelloWorld : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let! person = ctx.BindJsonAsync<Person>()
let greeting = sprintf "Hello World, from %s" person.Name
return! text greeting next ctx
}
Starting with version 3.5.0 and onwards Giraffe exposes an additional convenience function which can be used to generate new handler functions which only require access to the HttpContext
object without having to define the full verbose implementation as shown above.
The handleContext
function can be used like this:
let handlerWithLogging : HttpHandler =
handleContext(
fun ctx ->
let logger = ctx.GetService<ILogger>()
logger.LogInformation("From the context")
ctx.WriteTextAsync "")
Or alternatively if additional asynchronous work needs to be done:
let handlerWithLogging2 : HttpHandler =
handleContext(
fun ctx ->
task {
let logger = ctx.GetService<ILogger>()
logger.LogInformation("From the context")
// Do more async stuff
return! ctx.WriteTextAsync "Done working"
})
Please note that the handleContext
function doesn't have control over the next
handler and therefore cannot "skip" the handler pipeline like normal HttpHandler
functions can do (see: Continue vs. Return vs. Skip).
Please be also aware that a Task<'T>
in .NET is just a promise of 'T
when a task eventually finishes asynchronously. Unless you define an HttpHandler
function in the most verbose way (with the task {}
CE) and actively await a nested result with either let!
or return!
then the handler will not wait for the task to complete before returning to the GiraffeMiddleware
.
This has important implications if you want to execute code in an HttpHandler
after invoking the next handler, such as cleaning up resources with the use
keyword. For example, in the code below, the IDisposable
will get disposed before the actual handler
gets executed. This is because a HttpHandler
is a HttpFunc -> HttpContext -> Task<HttpContext option>
and therefore handler next ctx
only returns a Task<HttpContext option>
which hasn't been completed yet:
let doSomething handler : HttpHandler =
fun next ctx ->
use __ = somethingToBeDisposedAtTheEndOfTheRequest
handler next ctx
However, by explicitly invoking the handler
from within a task {}
CE one can ensure that the handler
gets executed before the IDisposable
gets disposed:
let doSomething handler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
use __ = somethingToBeDisposedAtTheEndOfTheRequest
return! handler next ctx
}
In Giraffe there are three scenarios which a given HttpHandler
can invoke:
- Continue with next handler
- Return early
- Skip
A handler performs some actions on the HttpRequest
and/or HttpResponse
object and then invokes the next
handler to continue with the pipeline.
A great example is the setHttpHeader
handler, which sets a given HTTP header and afterwards always calls into the next
http handler:
let setHttpHeader key value : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
ctx.SetHttpHeader key value
next ctx
Sometimes an HttpHandler
wants to return early and not continue with the remaining HttpHandler
pipeline.
A typical example would be an authentication or authorization handler, which would not continue with the remaining pipeline if a user wasn't authenticated. Instead it might want to return a 401 Unauthorized
response:
let earlyReturn : HttpFunc = Some >> Task.FromResult
let checkUserIsLoggedIn : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
if isNotNull ctx.User && ctx.User.Identity.IsAuthenticated
then next ctx
else setStatusCode 401 earlyReturn ctx
In the else
clause the checkUserIsLoggedIn
handler returns a 401 Unauthorized
HTTP response and skips the remaining HttpHandler
pipeline by not invoking next
but an already completed task.
If you were to have an HttpHandler
defined with the task {}
CE then you could alternatively also return Some HttpContext
in order to return early:
let checkUserIsLoggedIn : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
if isNotNull ctx.User && ctx.User.Identity.IsAuthenticated
then return! next ctx
else
ctx.SetStatusCode 401
return Some ctx
}
There may be cases where an HttpHandler
function might conclude that a given HttpRequest
should not be handled by the handler or the remaining HttpHandler
pipeline. In such a case an HttpHandler
can skip the pipeline and defer the handling of the web request to either another HttpHandler
function (when nested in a choose
handler) or to another ASP.NET Core middleware altogether.
The GET
handler is a good example of such a scenario. If a web request doesn't match the specified HTTP verb then the handler will skip the subsequent pipeline and defer to another handler or another ASP.NET Core middleware:
let skip : HttpFuncResult = Task.FromResult None
let GET : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
if HttpMethods.IsGet ctx.Request.Method
then next ctx
else skip
If you were to have an HttpHandler
defined with the task {}
CE then you could alternatively also return None
in order to skip the remaining pipeline:
let GET : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
if HttpMethods.IsGet ctx.Request.Method
then return! next ctx
else return None
}
Install the Giraffe NuGet package:
PM> Install-Package Giraffe
Create a web application and plug it into the ASP.NET Core middleware:
open Giraffe
let webApp =
choose [
route "/ping" >=> text "pong"
route "/" >=> htmlFile "/pages/index.html" ]
type Startup() =
member __.ConfigureServices (services : IServiceCollection) =
// Register default Giraffe dependencies
services.AddGiraffe() |> ignore
member __.Configure (app : IApplicationBuilder)
(env : IHostingEnvironment)
(loggerFactory : ILoggerFactory) =
// Add Giraffe to the ASP.NET Core pipeline
app.UseGiraffe webApp
[<EntryPoint>]
let main _ =
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(
fun webHostBuilder ->
webHostBuilder
.UseStartup<Startup>()
|> ignore)
.Build()
.Run()
0
Instead of creating a Startup
class you can also add Giraffe in a more functional way:
open Giraffe
let webApp =
choose [
route "/ping" >=> text "pong"
route "/" >=> htmlFile "/pages/index.html" ]
let configureApp (app : IApplicationBuilder) =
// Add Giraffe to the ASP.NET Core pipeline
app.UseGiraffe webApp
let configureServices (services : IServiceCollection) =
// Add Giraffe dependencies
services.AddGiraffe() |> ignore
[<EntryPoint>]
let main _ =
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(
fun webHostBuilder ->
webHostBuilder
.Configure(configureApp)
.ConfigureServices(configureServices)
|> ignore)
.Build()
.Run()
0
ASP.NET Core has built in dependency management which works out of the box with Giraffe.
Registering services is done the same way as it is done for any other ASP.NET Core web application:
let configureServices (services : IServiceCollection) =
// Add default Giraffe dependencies
services.AddGiraffe() |> ignore
// Add other dependencies
// ...
[<EntryPoint>]
let main _ =
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(
fun webHostBuilder ->
webHostBuilder
.Configure(configureApp)
// Calling ConfigureServices to set up dependencies
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
|> ignore)
.Build()
.Run()
0
Retrieving registered services from within a Giraffe HttpHandler
function is done through the built in service locator (RequestServices
) which comes with an HttpContext
object:
let someHttpHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
let fooBar =
ctx.RequestServices.GetService(typeof<IFooBar>)
:?> IFooBar
// Do something with `fooBar`...
// Return a Task<HttpContext option>
Giraffe has an additional HttpContext
extension method called GetService<'T>
to make the code less cumbersome:
let someHttpHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
let fooBar = ctx.GetService<IFooBar>()
// Do something with `fooBar`...
// Return a Task<HttpContext option>
There's a handful more extension methods available to retrieve a few default dependencies like an IHostingEnvironment
or ILogger
object which are covered in the respective sections of this document.
ASP.NET Core has built in support for working with multiple environments and configuration management, which both work out of the box with Giraffe.
Additionally Giraffe exposes a GetWebHostEnvironment()
extension method which can be used to easier retrieve an IWebHostEnvironment
object from within an HttpHandler
function:
let someHttpHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
let env = ctx.GetWebHostEnvironment()
// Do something with `env`...
// Return a Task<HttpContext option>
Configuration options can be retrieved via the GetService<'T>
extension method:
let someHttpHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
let settings = ctx.GetService<IOptions<MySettings>>()
// Do something with `settings`...
// Return a Task<HttpContext option>
If you need to access the configuration when configuring services, you can access it like this:
let configureServices (services : IServiceCollection) =
let serviceProvider = services.BuildServiceProvider()
let settings = serviceProvider.GetService<IConfiguration>()
// Configure services using the `settings`...
services.AddGiraffe() |> ignore
ASP.NET Core has a built in Logging API which works out of the box with Giraffe.
One or more logging providers can be configured during application startup:
let configureLogging (builder : ILoggingBuilder) =
// Set a logging filter (optional)
let filter (l : LogLevel) = l.Equals LogLevel.Error
// Configure the logging factory
builder.AddFilter(filter) // Optional filter
.AddConsole() // Set up the Console logger
.AddDebug() // Set up the Debug logger
// Add additional loggers if wanted...
|> ignore
[<EntryPoint>]
let main _ =
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(
fun webHostBuilder ->
webHostBuilder
.Configure(configureApp)
.ConfigureServices(configureServices)
// Calling ConfigureLogging to set up logging providers
.ConfigureLogging(configureLogging)
|> ignore)
.Build()
.Run()
0
Just like dependency management the logging API is configured the same way as it is done for any other ASP.NET Core web application.
After one or more logging providers have been configured you can retrieve an ILogger
object (which can be used for logging) through the GetLogger<'T>()
or GetLogger (categoryName : string)
extension methods:
let someHttpHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
// Retrieve an ILogger through one of the extension methods
let loggerA = ctx.GetLogger<ModuleName>()
let loggerB = ctx.GetLogger("someHttpHandler")
// Log some data
loggerA.LogCritical("Something critical")
loggerB.LogInformation("Logging some random info")
// etc.
// Return a Task<HttpContext option>
Giraffe exposes a separate error handling middleware which can be used to configure a functional error handler, which can react to any unhandled exception of the entire ASP.NET Core web application.
The Giraffe ErrorHandler
function accepts an Exception
object and a default ILogger
and returns an HttpHandler
function:
type ErrorHandler = exn -> ILogger -> HttpHandler
Because the Giraffe ErrorHandler
returns an HttpHandler
function it is possible to create anything from a simple error handling function to a complex error handling application.
This simple errorHandler
function writes the entire Exception
object to the logs, clears the response object and returns an HTTP 500 server error response:
let errorHandler (ex : Exception) (logger : ILogger) =
logger.LogError(EventId(), ex, "An unhandled exception has occurred while executing the request.")
clearResponse
>=> ServerErrors.INTERNAL_ERROR ex.Message
In order to enable the error handler you have to configure the GiraffeErrorHandlerMiddleware
in your application startup:
// Define the error handler function
let errorHandler (ex : Exception) (logger : ILogger) =
logger.LogError(EventId(), ex, "An unhandled exception has occurred while executing the request.")
clearResponse
>=> ServerErrors.INTERNAL_ERROR ex.Message
// Register all ASP.NET Core middleware
let configureApp (app : IApplicationBuilder) =
// Register the error handler first, so that all exceptions from other middleware can bubble up and be caught by the ErrorHandler function:
app.UseGiraffeErrorHandler(errorHandler)
.UseGiraffe webApp
[<EntryPoint>]
let main _ =
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(
fun webHostBuilder ->
webHostBuilder
// Calling Configure to set up all middleware
.Configure(configureApp)
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
|> ignore)
.Build()
.Run()
0
... or the equivalent by using a Startup
class:
type Startup() =
member __.ConfigureServices (services : IServiceCollection) =
// Register default Giraffe dependencies
services.AddGiraffe() |> ignore
member __.Configure (app : IApplicationBuilder)
(env : IHostingEnvironment)
(loggerFactory : ILoggerFactory) =
app.UseGiraffeErrorHandler errorHandler
.UseGiraffe webApp
[<EntryPoint>]
let main _ =
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(
fun webHostBuilder ->
webHostBuilder
.UseStartup<Startup>()
|> ignore)
.Build()
.Run()
0
It is recommended to set the error handler as the first middleware in the ASP.NET Core pipeline, so that any unhandled exception from other middleware can be caught and processed by the error handling function.
Giraffe comes with a large set of default HttpContext
extension methods as well as default HttpHandler
functions which can be used to build rich web applications.
Working with HTTP headers in Giraffe is plain simple. The TryGetRequestHeader (key : string)
extension method tries to retrieve the value of a given HTTP header and then returns either Some string
or None
:
let someHttpHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
let someValue =
match ctx.TryGetRequestHeader "X-MyOwnHeader" with
| None -> "default value"
| Some headerValue -> headerValue
// Do something with `someValue`...
// Return a Task<HttpContext option>
This method is useful when trying to retrieve optional HTTP headers from within an HttpHandler
.
If an HTTP header is mandatory then the GetRequestHeader (key : string)
extension method might be a better fit. Instead of returning an Option<string>
object it will return a Result<string, string>
type:
let someHttpHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
match ctx.GetRequestHeader "X-MyOwnHeader" with
| Error msg ->
// Mandatory header is missing.
// Log error message
// Return error response to the client.
| Ok headerValue ->
// Do something with `headerValue`...
// Return a Task<HttpContext option>
Setting an HTTP header in the response can be done via the SetHttpHeader (key : string) (value : obj)
extension method:
let someHttpHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
ctx.SetHttpHeader "X-CustomHeader" "some-value"
// Do other stuff...
// Return a Task<HttpContext option>
You can also set an HTTP header via the setHttpHeader
http handler:
let notFoundHandler : HttpHandler =
setHttpHeader "X-CustomHeader" "Some value"
>=> RequestErrors.NOT_FOUND "Not Found"
let webApp =
choose [
route "/foo" >=> text "Foo"
route "/bar" >=> text "Bar"
notFoundHandler
]
Please note that these are additional Giraffe functions which complement already existing HTTP header functionality in the ASP.NET Core framework. ASP.NET Core offers higher level HTTP header functionality through the ctx.Request.GetTypedHeaders()
method.
Giraffe exposes a set of HttpHandler
functions which can filter a request based on the request's HTTP verb:
GET
POST
PUT
PATCH
DELETE
HEAD
OPTIONS
TRACE
CONNECT
There is an additional GET_HEAD
handler which can filter an HTTP GET
and HEAD
request at the same time.
Filtering requests based on their HTTP verb can be useful when implementing a route which should behave differently based on the verb (e.g. GET
vs. POST
):
let submitFooHandler : HttpHandler =
// Do something
let submitBarHandler : HttpHandler =
// Do something
let webApp =
choose [
// Filters for GET requests
GET >=> choose [
route "/foo" >=> text "Foo"
route "/bar" >=> text "Bar"
]
// Filters for POST requests
POST >=> choose [
route "/foo" >=> submitFooHandler
route "/bar" >=> submitBarHandler
]
// If the HTTP verb or the route didn't match return a 404
RequestErrors.NOT_FOUND "Not Found"
]
If you need to check the request's HTTP verb from within an HttpHandler
function then you can use the default ASP.NET Core HttpMethods
class:
open Microsoft.AspNetCore.Http
let someHttpHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
if HttpMethods.IsPut ctx.Request.Method then
// Do something
else
// Do something else
// Return a Task<HttpContext option>
The GET_HEAD
handler is a special handler which can be used to enable GET
and HEAD
requests on a resource at the same time. This can be very useful when caching is enabled and clients might want to send HEAD
requests to check the ETag
or Last-Modified
HTTP headers before issuing a GET
.
More combinations can be easily created via the choose
http handler:
let POST_HEAD : HttpHandler = choose [ POST; HEAD ]
Setting the HTTP status code of a response can be done either via the SetStatusCode (httpStatusCode : int)
extension method or with the setStatusCode (statusCode : int)
function:
let someHttpHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
ctx.SetStatusCode 200
// Return a Task<HttpContext option>
// or...
let someHttpHandler : HttpHandler =
setStatusCode 200
>=> text "Hello World"
Giraffe also offers a default set of pre-defined HttpHandler
functions, which can be used to return a response with a specific HTTP status code.
These HttpHandler
functions are categorised in four sub modules:
- Intermediate (1xx status codes)
- Successful (2xx status codes)
- RequestErrors (4xx status codes)
- ServerErrors (5xx status codes)
For the majority of status code HttpHandler
functions (except the Intermediate
module) there are two versions for each individual status code available - a lower case and an upper case function (e.g. Successful.ok
and Successful.OK
).
The lower case version lets you combine the HttpHandler
function with another HttpHandler
function:
Successful.ok (text "Hello World")
This is a shorter (and more explicit) version of:
setStatusCode 200 >=> text "Hello World"
On the other hand the upper case version can be used to send an object directly to the client:
[<CLIMutable>]
type Person =
{
FirstName : string
LastName : string
}
let johnDoe =
{
FirstName = "John"
LastName = "Doe"
}
let app = choose [
route `/` >=> Successful.OK "Hello World"
route `/john` >=> Successful.OK johnDoe
]
The upper case function is the equivalent and shorter version of:
setStatusCode 200 >=> negotiate johnDoe
The negotiate
handler attempts to return an object back to the client based on the client's accepted mime types (see Content Negotiation).
The following sub modules and status code HttpHandler
functions are available out of the box:
Please note that there is no module for 3xx
HTTP status codes available, instead it is recommended to use the redirectTo
http handler for redirection functionality (see Redirection).
HTTP Status Code | Function name | Example |
---|---|---|
100 | CONTINUE | route "/" >=> Intermediate.CONTINUE |
101 | SWITCHING_PROTO | route "/" >=> Intermediate.SWITCHING_PROTO |
HTTP Status Code | Function name | Example |
---|---|---|
200 | ok | route "/" >=> Successful.ok (text "Hello World") |
200 | OK | route "/" >=> Successful.OK "Hello World" |
201 | created | route "/" >=> Successful.created (json someObj) |
201 | CREATED | route "/" >=> Successful.CREATED someObj |
202 | accepted | route "/" >=> Successful.accepted (xml someObj) |
202 | ACCEPTED | route "/" >=> Successful.ACCEPTED someObj |
204 | NO_CONTENT | route "/" >=> Successful.NO_CONTENT |
HTTP Status Code | Function name | Example |
---|---|---|
400 | badRequest | route "/" >=> RequestErrors.badRequest (text "Don't like it") |
400 | BAD_REQUEST | route "/" >=> RequestErrors.BAD_REQUEST "Don't like it" |
401 | unauthorized | route "/" >=> RequestErrors.unauthorized "Basic" "MyApp" (text "Don't know who you are") |
401 | UNAUTHORIZED | route "/" >=> RequestErrors.UNAUTHORIZED "Basic" "MyApp" "Don't know who you are" |
403 | forbidden | route "/" >=> RequestErrors.forbidden (text "Not enough permissions") |
403 | FORBIDDEN | route "/" >=> RequestErrors.FORBIDDEN "Not enough permissions" |
404 | notFound | route "/" >=> RequestErrors.notFound (text "Page not found") |
404 | NOT_FOUND | route "/" >=> RequestErrors.NOT_FOUND "Page not found" |
405 | methodNotAllowed | route "/" >=> RequestErrors.methodNotAllowed (text "Don't support this") |
405 | METHOD_NOT_ALLOWED | route "/" >=> RequestErrors.METHOD_NOT_ALLOWED "Don't support this" |
406 | notAcceptable | route "/" >=> RequestErrors.notAcceptable (text "Not having this") |
406 | NOT_ACCEPTABLE | route "/" >=> RequestErrors.NOT_ACCEPTABLE "Not having this" |
409 | conflict | route "/" >=> RequestErrors.conflict (text "some conflict") |
409 | CONFLICT | route "/" >=> RequestErrors.CONFLICT "some conflict" |
410 | gone | route "/" >=> RequestErrors.gone (text "Too late, not here anymore") |
410 | GONE | route "/" >=> RequestErrors.GONE "Too late, not here anymore" |
415 | unsupportedMediaType | route "/" >=> RequestErrors.unsupportedMediaType (text "Please send in different format") |
415 | UNSUPPORTED_MEDIA_TYPE | route "/" >=> RequestErrors.UNSUPPORTED_MEDIA_TYPE "Please send in different format" |
422 | unprocessableEntity | route "/" >=> RequestErrors.unprocessableEntity (text "Can't do anything with this") |
422 | UNPROCESSABLE_ENTITY | route "/" >=> RequestErrors.UNPROCESSABLE_ENTITY "Can't do anything with this" |
428 | preconditionRequired | route "/" >=> RequestErrors.preconditionRequired (test "Please do something else first") |
428 | PRECONDITION_REQUIRED | route "/" >=> RequestErrors.PRECONDITION_REQUIRED "Please do something else first" |
429 | tooManyRequests | route "/" >=> RequestErrors.tooManyRequests (text "Slow down champ") |
429 | TOO_MANY_REQUESTS | route "/" >=> RequestErrors.TOO_MANY_REQUESTS "Slow down champ" |
Note that the unauthorized
and UNAUTHORIZED
functions require two additional parameters, an authentication scheme and a realm.
HTTP Status Code | Function name | Example |
---|---|---|
500 | internalError | route "/" >=> ServerErrors.internalError (text "Ops, something went wrong") |
500 | INTERNAL_ERROR | route "/" >=> ServerErrors.INTERNAL_ERROR "Not implemented" |
501 | notImplemented | route "/" >=> ServerErrors.notImplemented (text "Not implemented") |
501 | NOT_IMPLEMENTED | route "/" >=> ServerErrors.NOT_IMPLEMENTED "Ops, something went wrong" |
502 | badGateway | route "/" >=> ServerErrors.badGateway (text "Bad gateway") |
502 | BAD_GATEWAY | route "/" >=> ServerErrors.BAD_GATEWAY "Bad gateway" |
503 | serviceUnavailable | route "/" >=> ServerErrors.serviceUnavailable (text "Service unavailable") |
503 | SERVICE_UNAVAILABLE | route "/" >=> ServerErrors.SERVICE_UNAVAILABLE "Service unavailable" |
504 | gatewayTimeout | route "/" >=> ServerErrors.gatewayTimeout (text "Gateway timeout") |
504 | GATEWAY_TIMEOUT | route "/" >=> ServerErrors.GATEWAY_TIMEOUT "Gateway timeout" |
505 | invalidHttpVersion | route "/" >=> ServerErrors.invalidHttpVersion (text "Invalid HTTP version") |
Giraffe offers a variety of routing HttpHandler
functions to accommodate the majority of use cases.
The simplest form of routing can be done with the route
http handler:
let webApp =
choose [
route "/foo" >=> text "Foo"
route "/bar" >=> text "Bar"
// If none of the routes matched then return a 404
RequestErrors.NOT_FOUND "Not Found"
]
The route
http handler expects an exact match. If the HTTP request was made to a slightly different route (e.g. /Bar
or /bAr
) then the route "/bar"
handler will not serve the request.
This can be avoided by using the case insensitive routeCi
http handler:
let webApp =
choose [
route "/foo" >=> text "Foo"
routeCi "/foo" >=> text "Bar"
// If none of the routes matched then return a 404
RequestErrors.NOT_FOUND "Not Found"
]
In the example above a request made to https://example.org/FOO
would return Bar
in the response.
According to the HTTP specification a route with a trailing slash is not equivalent to the same route without a trailing slash:
https://example.org/foo
https://example.org/foo/
A web server might (rightfully) want to serve a different response for each route:
let webApp =
choose [
route "/foo" >=> text "Foo"
route "/foo/" >=> text "Bar"
// If none of the routes matched then return a 404
RequestErrors.NOT_FOUND "Not Found"
]
However many web applications choose to treat both routes as the same. If you would like to achieve this behaviour by using a single route in Giraffe then you can use the routex
http handler which accepts a Regex
string for matching routes:
let webApp =
choose [
routex "/foo(/?)" >=> text "Bar"
// If none of the routes matched then return a 404
RequestErrors.NOT_FOUND "Not Found"
]
The (/?)
regex pattern specifies that a /
can occur zero or one time at the end of the route, which means it would successfully match the following two routes:
https://example.org/foo
https://example.org/foo/
However, this example wouldn't match a request made to https://example.org/foo///
. If you want to match any number of trailing slashes then you must use (/*)
instead:
let webApp =
choose [
routex "/foo(/*)" >=> text "Bar"
// If none of the routes matched then return a 404
RequestErrors.NOT_FOUND "Not Found"
]
Please be aware that such a routex
can create a conflict and unexpected behaviour if you have a similar matching routef
(see routef):
let webApp =
choose [
routex "/foo(/*)" >=> text "Bar"
routef "/foo/%s/%s/%s" (fun (s1, s2, s3) -> text (sprintf "%s%s%s" s1 s2 s3))
// If none of the routes matched then return a 404
RequestErrors.NOT_FOUND "Not Found"
]
In the above scenario it is not clear which one of the two http handlers a user want to be invoked when a request is made to https://example.org/foo///
.
If you want to learn more about Regex
please check the Regular Expression Language Reference.
The routexp
http handler is a combination of routex
and routef
. It resolves a route exactly like routex
, but then passes the resolved Regex Groups as a Seq<string>
parameter into the supplied handler function similar to how routef
invokes the next handler in the pipeline.
The routeCix
http handler is the case insensitive version of routex
:
let webApp =
choose [
routex "/foo(/?)" >=> text "Foo"
routeCix "/foo(/?)" >=> text "Bar"
// If none of the routes matched then return a 404
RequestErrors.NOT_FOUND "Not Found"
]
In the example above a request made to https://example.org/FOO/
would return Bar
in the response.
If a route contains user defined parameters then the routef
http handler can be handy:
let fooHandler (first : string,
last : string,
age : int)
: HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
(sprintf "First: %s, Last: %s, Age: %i" first last age
|> text) next ctx
let webApp =
choose [
routef "/foo/%s/%s/%i" fooHandler
routef "/bar/%O" (fun guid -> text (guid.ToString()))
// If none of the routes matched then return a 404
RequestErrors.NOT_FOUND "Not Found"
]
The routef
http handler takes two parameters - a format string and an HttpHandler
function.
The format string supports the following format chars:
Format Char | Type |
---|---|
%b |
bool |
%c |
char |
%s |
string |
%i |
int |
%d |
int64 |
%f |
float /double |
%O |
Guid (including short GUIDs*) |
%u |
uint64 (formatted as a short ID*) |
*) Please note that the %O
and %u
format characters also support URL friendly short GUIDs and IDs.
The %O
format character supports GUIDs in the format of:
00000000000000000000000000000000
00000000-0000-0000-0000-000000000000
Xy0MVKupFES9NpmZ9TiHcw
The last string represents an example of a Short GUID which is a normal GUID shortened into a URL encoded 22 character long string. Routes which use the %O
format character will be able to automatically resolve a Short GUID as well as a normal GUID into a System.Guid
argument.
The %u
format character can only resolve an 11 character long Short ID (aka YouTube ID) into a uint64
value.
Short GUIDs and short IDs are popular choices to make URLs shorter and friendlier whilst still mapping to a unique System.Guid
or uint64
value on the server side.
Short GUIDs and IDs can also be resolved from query string parameters by making use of the ShortGuid
and ShortId
helper modules.
The case insensitive version of routef
is routeCif
:
let webApp =
choose [
routeCif "/foo/%s/bar" (fun str -> text str)
// If none of the routes matched then return a 404
RequestErrors.NOT_FOUND "Not Found"
]
Please be aware that a case insensitive URL matching will return unexpected results in combination with case sensitive arguments such as short GUIDs and short IDs.
If you need to bind route parameters directly to a type then you can use the routeBind<'T>
http handler. Unlike routef
or routeCif
which work with a format string the routeBind<'T>
http handler tries to match named parameters to the properties of a given type 'T
:
[<CLIMutable>]
type Person =
{
FirstName : string
LastName : string
}
let personHandler (person : Person) : HttpHandler =
sprintf "Hello %s %s" person.FirstName person.LastName
|> Successful.OK
let webApp =
choose [
routeBind<Person> "/p/{firstName}/{lastName}" personHandler
]
The routeBind<'T>
http handler can also contain valid Regex
code to match a variety of different routes.
For example by definition (according to the spec) a route with a trailing slash is not the same as the equivalent route without a trailing slash. Therefore it is perfectly valid if a web server doesn't serve (or serves a different response) for the following two routes:
/p/{firstName}/{lastName}
/p/{firstName}/{lastName}/
However many web applications choose to treat both URLs as the same. The routeBind<'T>
http handler can make use of Regex
to enable such use cases:
[<CLIMutable>]
type Blah =
{
Foo : string
Bar : string
}
let blahHandler (blah : Blah) : HttpHandler =
sprintf "Hello %s %s" blah.Foo blah.Bar
|> Successful.OK
let webApp =
choose [
routeBind<Blah> "/p/{foo}/{bar}(/?)" blahHandler
]
By appending the Regex
code (/?)
to the end of the route we tell the routeBind<'T>
handler to match any route which has either zero or one trailing slash.
If any number of trailing slashes should be allowed then you can swap the ?
with a *
in the Regex
:
routeBind<Blah> "/p/{foo}/{bar}(/*)" blahHandler
For a complete list of valid Regex
codes please visit the official Regular Expression Language Reference.
Sometimes it can be useful to pre-filter a route in order to enable certain functionality which should only be applied to a specific collection of routes.
The routeStartsWith
http handler does just that:
let webApp =
routeStartsWith "/api/" >=>
// Pre-filter because only API calls require Auth
requiresAuthentication (challenge "Cookie") >=>
choose [
route "/api/v1/foo" >=> text "Foo"
route "/api/v1/bar" >=> text "Bar"
]
The case insensitive version of routeStartsWith
is routeStartsWithCi
:
let webApp =
routeStartsWithCi "/api/" >=>
// Pre-filter because only API calls require Auth
requiresAuthentication (challenge "Cookie") >=>
choose [
route "/api/v1/foo" >=> text "Foo"
route "/api/v1/bar" >=> text "Bar"
]
Please note that the routeStartsWith
and routeStartsWithCi
http handlers do not change how subsequent routing functions are matched. The final URL to get a "Foo" response is still http[s]://your-domain.com/api/v1/foo
(single /api
) and not http[s]://your-domain.com/api/api/v1/foo
(double /api
).
In contrast to routeStartsWith
the subRoute
http handler lets you categorise routes without having to repeat already pre-filtered parts of the route:
let webApp =
subRoute "/api"
(choose [
subRoute "/v1"
(choose [
route "/foo" >=> text "Foo 1"
route "/bar" >=> text "Bar 1" ])
subRoute "/v2"
(choose [
route "/foo" >=> text "Foo 2"
route "/bar" >=> text "Bar 2" ]) ])
In this example the final URL to retrieve "Bar 2" would be http[s]://your-domain.com/api/v2/bar
.
As you might expect the subRouteCi
http handler is the case insensitive version of subRoute
:
let webApp =
subRouteCi "/api"
(choose [
subRouteCi "/v1"
(choose [
route "" >=> text "Default"
route "/foo" >=> text "Foo 1"
route "/bar" >=> text "Bar 1" ])
subRouteCi "/v2"
(choose [
route "/foo" >=> text "Foo 2"
route "/bar" >=> text "Bar 2" ]) ])
Please note that only the path specified for subRouteCi
is case insensitive. Nested routes after subRouteCi
will be evaluated as per definition of each individual route.
Note: If you wish to have a default route for any subRoute
handler (e.g. /api/v1
from the above example) then you need to specify the route as route ""
and not as route "/"
, because /api/v1/
is a fundamentally different than /api/v1
according to the HTTP specification.
The subRoutef
http handler is a combination of the routef
and the subRoute
http handler:
let app =
GET >=> choose [
route "/" >=> text "index"
route "/foo" >=> text "bar"
subRoutef "/%s/api" (fun lang ->
requiresAuthentication (challenge "Cookie") >=>
choose [
route "/blah" >=> text "blah"
routef "/%s" (fun n -> text (sprintf "Hello %s! Lang: %s" n lang))
])
setStatusCode 404 >=> text "Not found" ]
This can be useful when an application has dynamic parameters at the beginning of each route (e.g. language parameter):
https://example.org/en/users/John
https://example.org/de/users/Ryan
https://example.org/fr/users/Nicky
...
If your web server is listening to multiple ports through WebHost.UseUrls
then you can use the routePorts
http handler to filter incoming requests based on their port:
let guiApp =
choose [
route "/" >=> text "Hello World"
routeCi "/foo" >=> text "Bar"
]
let apiApp =
subRoute "/api"
(choose [
subRoute "/v1"
(choose [
route "/foo" >=> text "Foo 1"
route "/bar" >=> text "Bar 1" ])
subRoute "/v2"
(choose [
route "/foo" >=> text "Foo 2"
route "/bar" >=> text "Bar 2" ]) ])
let webApp =
routePorts [
(9001, guiApp)
(9002, apiApp)
]
Working with query strings is very similar to working with HTTP headers in Giraffe. The TryGetQueryStringValue (key : string)
extension method tries to retrieve the value of a given query string parameter and then returns either Some string
or None
:
let someHttpHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
let someValue =
match ctx.TryGetQueryStringValue "q" with
| None -> "default value"
| Some q -> q
// Do something with `someValue`...
// Return a Task<HttpContext option>
This method is useful when trying to retrieve optional query string parameters from within an HttpHandler
.
If a query string parameter is mandatory then the GetQueryStringValue (key : string)
extension method might be a better fit. Instead of returning an Option<string>
object it will return a Result<string, string>
type:
let someHttpHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
match ctx.GetQueryStringValue "q" with
| Error msg ->
// Mandatory query string value is missing.
// Log error message
// Return error response to the client.
| Ok q ->
// Do something with `q`...
// Return a Task<HttpContext option>
You can also access the query string through the ctx.Request.Query
object which returns an IQueryCollection
object which allows you to perform more actions on it.
Last but not least there is also an HttpContext
extension method called BindQueryString<'T>
which lets you bind an entire query string to an object of type 'T
(see Binding Query Strings).
Giraffe offers out of the box a few default HttpContext
extension methods and equivalent HttpHandler
functions which make it possible to bind the payload or query string of an HTTP request to a custom object.
The BindJsonAsync<'T>()
extension method can be used to bind a JSON payload to an object of type 'T
:
[<CLIMutable>]
type Car =
{
Name : string
Make : string
Wheels : int
Built : DateTime
}
let submitCar : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
// Binds a JSON payload to a Car object
let! car = ctx.BindJsonAsync<Car>()
// Sends the object back to the client
return! Successful.OK car next ctx
}
let webApp =
choose [
GET >=>
choose [
route "/" >=> text "index"
route "ping" >=> text "pong"
]
POST >=> route "/car" >=> submitCar
]
Alternatively you can also use the bindJson<'T>
http handler:
[<CLIMutable>]
type Car =
{
Name : string
Make : string
Wheels : int
Built : DateTime
}
let webApp =
choose [
GET >=>
choose [
route "/" >=> text "index"
route "ping" >=> text "pong"
]
POST
>=> route "/car"
>=> bindJson<Car> (fun car -> Successful.OK car)
]
Both, the HttpContext
extension method as well as the HttpHandler
function will try to create an instance of type 'T
regardless if the submitted payload contained a complete representation of 'T
or not. The parsed object might only contain partial data (where some properties might be null
) and additional null
checks might be required before further processing.
Please note that in order for the model binding to work the record type must be decorated with the [<CLIMutable>]
attribute, which will make sure that the type will have a parameterless constructor.
The underlying JSON serializer can be configured as a dependency during application startup (see JSON).
The BindXmlAsync<'T>()
extension method binds an XML payload to an object of type 'T
:
[<CLIMutable>]
type Car =
{
Name : string
Make : string
Wheels : int
Built : DateTime
}
let submitCar : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
// Binds an XML payload to a Car object
let! car = ctx.BindXmlAsync<Car>()
// Sends the object back to the client
return! Successful.OK car next ctx
}
let webApp =
choose [
GET >=>
choose [
route "/" >=> text "index"
route "ping" >=> text "pong"
]
POST >=> route "/car" >=> submitCar
]
Alternatively you can also use the bindXml<'T>
http handler:
[<CLIMutable>]
type Car =
{
Name : string
Make : string
Wheels : int
Built : DateTime
}
let webApp =
choose [
GET >=>
choose [
route "/" >=> text "index"
route "ping" >=> text "pong"
]
POST
>=> route "/car"
>=> bindXml<Car> (fun car -> Successful.OK car)
]
Both, the HttpContext
extension method as well as the HttpHandler
function will try to create an instance of type 'T
regardless if the submitted payload contained a complete representation of 'T
or not. The parsed object might only contain partial data (where some properties might be null
) and additional null
checks might be required before further processing.
Please note that in order for the model binding to work the record type must be decorated with the [<CLIMutable>]
attribute, which will make sure that the type will have a parameterless constructor.
The underlying XML serializer can be configured as a dependency during application startup (see XML).
The BindFormAsync<'T> (?cultureInfo : CultureInfo)
extension method binds form data to an object of type 'T
. You can also specify an optional CultureInfo
object for parsing culture specific data such as DateTime
objects or floating point numbers:
[<CLIMutable>]
type Car =
{
Name : string
Make : string
Wheels : int
Built : DateTime
}
let submitCar : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
// Binds a form payload to a Car object
let! car = ctx.BindFormAsync<Car>()
// or with a CultureInfo:
let british = CultureInfo.CreateSpecificCulture("en-GB")
let! car2 = ctx.BindFormAsync<Car>(british)
// Sends the object back to the client
return! Successful.OK car next ctx
}
let webApp =
choose [
GET >=>
choose [
route "/" >=> text "index"
route "ping" >=> text "pong"
]
POST >=> route "/car" >=> submitCar
]
Alternatively you can use the bindForm<'T>
http handler (which also accepts an additional parameter of type CultureInfo option
):
[<CLIMutable>]
type Car =
{
Name : string
Make : string
Wheels : int
Built : DateTime
}
let british = CultureInfo.CreateSpecificCulture("en-GB")
let webApp =
choose [
GET >=>
choose [
route "/" >=> text "index"
route "ping" >=> text "pong"
]
POST
>=> route "/car"
>=> bindForm<Car> (Some british) (fun car -> Successful.OK car)
]
Just like in the previous examples the record type must be decorated with the [<CLIMutable>]
attribute in order for the model binding to work.
The BindFormAsync<'T>
extension method and the bindForm<'T>
http handler are both very loose model binding functions, which means they will try to create an instance of type 'T
even if some data was missing or provided in the wrong format (in which case it will just skip parsing the field).
While this has its own advantages it is not very idiomatic to functional programming.
For a more stricter (and more functional) model binding you can use the TryBindFormAsync<'T>
extension method or the tryBindForm<'T>
http handler function.
They are both very similar to the previous binding methods, except that they will not create an instance of type 'T
if the submitted payload did not contain all mandatory fields (any field which is not an F# option type) or had badly formatted data.
The TryBindFormAsync<'T>
method returns an object of type Result<'T, string>
. If the model binding was successful then the result will contain an instance of type 'T
, otherwise a string
value containing the parsing error message:
[<CLIMutable>]
type Car =
{
Name : string
Make : string
Wheels : int
Built : DateTime
}
let submitCar : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
// Binds a form payload to a Car object
let! result = ctx.TryBindFormAsync<Car>()
// or with a CultureInfo:
let british = CultureInfo.CreateSpecificCulture("en-GB")
let! result2 = ctx.TryBindFormAsync<Car>(british)
return!
(match result2 with
| Ok car -> Successful.OK car
| Error err -> RequestErrors.BAD_REQUEST err) next ctx
}
let webApp =
choose [
GET >=>
choose [
route "/" >=> text "index"
route "ping" >=> text "pong"
]
POST >=> route "/car" >=> submitCar
]
The tryBindForm<'T>
http handler is very similar, but instead of returning a Result<'T, string>
object it will invoke an error handler function if the model binding does not succeed:
[<CLIMutable>]
type Car =
{
Name : string
Make : string
Wheels : int
Built : DateTime
}
let british = CultureInfo.CreateSpecificCulture("en-GB")
let parsingError (err : string) = RequestErrors.BAD_REQUEST err
let webApp =
choose [
GET >=>
choose [
route "/" >=> text "index"
route "ping" >=> text "pong"
]
POST
>=> route "/car"
>=> tryBindForm<Car> parsingError (Some british) (fun car -> Successful.OK car)
RequestErrors.NOT_FOUND "Not found"
]
In this example if a Car
object could not be successfully created then the parsingError
handler will get invoked which will return an Http Bad Request
response with the parsing error message.
The BindQueryString<'T> (?cultureInfo : CultureInfo)
extension method binds query string parameters to an object of type 'T
. An optional CultureInfo
object can be specified for parsing culture specific data such as DateTime
objects and floating point numbers:
[<CLIMutable>]
type Car =
{
Name : string
Make : string
Wheels : int
Built : DateTime
}
let submitCar : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
// Binds the query string to a Car object
let car = ctx.BindQueryString<Car>()
// or with a CultureInfo:
let british = CultureInfo.CreateSpecificCulture("en-GB")
let car2 = ctx.BindQueryString<Car>(british)
// Sends the object back to the client
Successful.OK car next ctx
let webApp =
choose [
GET >=>
choose [
route "/" >=> text "index"
route "ping" >=> text "pong"
route "/car" >=> submitCar
]
]
Alternatively you can use the bindQuery<'T>
http handler (which also accepts an additional parameter of type CultureInfo option
):
[<CLIMutable>]
type Car =
{
Name : string
Make : string
Wheels : int
Built : DateTime
}
let british = CultureInfo.CreateSpecificCulture("en-GB")
let webApp =
choose [
GET >=>
choose [
route "/" >=> text "index"
route "ping" >=> text "pong"
]
POST
>=> route "/car"
>=> bindQuery<Car> (Some british) (fun car -> Successful.OK car)
]
Just like in the previous examples the record type must be decorated with the [<CLIMutable>]
attribute in order for the model binding to work.
The BindQueryString<'T>
extension method and the bindQuery<'T>
http handler are both very loose model binding functions, which means they will try to create an instance of type 'T
even if some data was missing or provided in the wrong format (in which case it will just skip parsing the field).
While this has its own advantages it is not very idiomatic to functional programming.
For a more stricter (and more functional) model binding approach you can use the TryBindQueryString<'T>
extension method or the tryBindQuery<'T>
http handler function.
They are both very similar to the previous binding methods, except that they will not create an instance of type 'T
if the submitted query string did not contain all mandatory fields (any field which is not an F# option type) or had badly formatted data.
The TryBindQueryString<'T>
method returns an object of type Result<'T, string>
. If the model binding was successful then the result will contain an instance of type 'T
, otherwise a string
value containing the parsing error message:
[<CLIMutable>]
type Car =
{
Name : string
Make : string
Wheels : int
Built : DateTime
}
let submitCar : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
// Binds a form payload to a Car object
let result = ctx.TryBindQueryString<Car>()
// or with a CultureInfo:
let british = CultureInfo.CreateSpecificCulture("en-GB")
let result2 = ctx.TryBindQueryString<Car>(british)
(match result2 with
| Ok car -> Successful.OK car
| Error err -> RequestErrors.BAD_REQUEST err) next ctx
let webApp =
choose [
GET >=>
choose [
route "/" >=> text "index"
route "ping" >=> text "pong"
]
POST >=> route "/car" >=> submitCar
]
The tryBindQuery<'T>
http handler is very similar, but instead of returning a Result<'T, string>
object it will invoke an error handler function if the model binding does not succeed:
[<CLIMutable>]
type Car =
{
Name : string
Make : string
Wheels : int
Built : DateTime
}
let british = CultureInfo.CreateSpecificCulture("en-GB")
let parsingError (err : string) = RequestErrors.BAD_REQUEST err
let webApp =
choose [
GET >=>
choose [
route "/" >=> text "index"
route "ping" >=> text "pong"
]
POST
>=> route "/car"
>=> tryBindQuery<Car> parsingError (Some british) (fun car -> Successful.OK car)
RequestErrors.NOT_FOUND "Not found"
]
In this example if a Car
object could not be successfully created then the parsingError
handler will get invoked which will return an Http Bad Request
response containing the parsing error message.
Special note
Aleksander Heintz has created a Gist which contains a re-usable query string API based on how Chiron works, which allows one doing something like the following:
type Report =
{ author: string option
project: string option
week: int option
summary: string option
progress: string list
comments: string list
plan: string list }
static member FromQuery (_: Report) =
fun author project week summary progress comments plan ->
{ author = author
project = project
week = week
summary = summary
progress = progress
comments = comments
plan = plan }
<!> Query.read "author"
<*> Query.read "project"
<*> Query.read "week"
<*> Query.read "summary"
<*> Query.read "progress"
<*> Query.read "comments"
<*> Query.read "plan"
let reportRoute = route "/report" >=> Query.bind (fun (r: Report) -> text <| sprintf "%A" r)
Even though this API didn't quite fit with Giraffe's existing tryBindQuery
and model validation function it is a nice example of how Giraffe can be extended to do similar things in different ways.
If you prefer this API you can either copy paste Aleksander's code from the provided Gist or find the contents of the Gist in the appendix of this document (in case the Gist gets ever deleted).
The BindModelAsync<'T> (?cultureInfo : CultureInfo)
method is a generic model binding function which will try to pick the right model parsing function based on a request's HTTP verb and Content-Type
header. With the help of BindModelAsync<'T>
it is possible to create a single endpoint which can bind JSON, XML, form and query string data:
[<CLIMutable>]
type Car =
{
Name : string
Make : string
Wheels : int
Built : DateTime
}
let submitCar : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
// Binds a Car object
let! car = ctx.BindModelAsync<Car>()
// or with a CultureInfo:
let british = CultureInfo.CreateSpecificCulture("en-GB")
let! car2 = ctx.BindModelAsync<Car>(british)
// Sends the object back to the client
return! Successful.OK car next ctx
}
let webApp =
choose [
GET >=>
choose [
route "/" >=> text "index"
route "ping" >=> text "pong"
]
route "/car" >=> submitCar
]
Alternatively you can use the bindModel<'T>
http handler:
[<CLIMutable>]
type Car =
{
Name : string
Make : string
Wheels : int
Built : DateTime
}
let british = CultureInfo.CreateSpecificCulture("en-GB")
let webApp =
choose [
GET >=>
choose [
route "/" >=> text "index"
route "ping" >=> text "pong"
]
POST
>=> route "/car"
>=> bindModel<Car> (Some british) (fun car -> Successful.OK car)
]
Again like before, the record type 'T
must be decorated with the [<CLIMutable>]
attribute in order for the model binding to work.
Giraffe exposes an IModelValidation<'T>
interface and an accompanying validateModel<'T>
http handler which can be used to validate a model in a more functional way.
Let's take a look at the following example:
[<CLIMutable>]
type Adult =
{
FirstName : string
MiddleName : string option
LastName : string
Age : int
}
override this.ToString() =
sprintf "Name: %s%s %s, Age: %i"
this.FirstName
(if this.MiddleName.IsSome then " " + this.MiddleName.Value else "")
this.LastName
this.Age
module WebApp =
let textHandler (x : obj) = text (x.ToString())
let parsingError err = RequestErrors.BAD_REQUEST err
let webApp _ =
choose [
route "/person"
>=> tryBindQuery<Adult> parsingError None textHandler
RequestErrors.NOT_FOUND "Not found"
]
The Adult
type is a normal F# record type which defines four properties (one of them optional) and an override of the ToString()
method.
The /person
route will try to bind a query string to an object of type Adult
before invoking the textHandler
which will eventually output the model by calling its ToString()
method.
The model has three mandatory properties (FirstName
, LastName
and Age
) and only one optional property MiddleName
, which means that a query string must contain at least the fields for the first- and last name, as well as the age for the model binding to succeed.
However, what if someone wants to define additional validation logic before responding with an Http 200
to a client?
For example the Adult
type could have an additional validation method called HasErrors
:
[<CLIMutable>]
type Adult =
{
FirstName : string
MiddleName : string option
LastName : string
Age : int
}
override this.ToString() =
sprintf "Name: %s%s %s, Age: %i"
this.FirstName
(if this.MiddleName.IsSome then " " + this.MiddleName.Value else "")
this.LastName
this.Age
member this.HasErrors() =
if this.FirstName.Length < 3 then Some "First name is too short."
else if this.FirstName.Length > 50 then Some "First name is too long."
else if this.LastName.Length < 3 then Some "Last name is too short."
else if this.LastName.Length > 50 then Some "Last name is too long."
else 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
The HasErrors
method is checking business logic which is specific to the type Adult
. For instance if Age
is less than 18 then the person is not an adult and therefore HasErrors
would return an F# option type with Some "Person must be an adult (age >= 18)."
.
It is a generic validation method which can be used from anywhere in an F# application to validate if a given Adult
object has logically correct data.
In order to make use of that validation method from within a Giraffe HttpHandler
one could create a custom handler to invoke the method:
module WebApp =
let adultHandler (adult : Adult) : HttpHandler =
match adult.HasErrors() with
| Some msg -> RequestErrors.BAD_REQUEST msg
| None -> text (adult.ToString())
let parsingError err = RequestErrors.BAD_REQUEST err
let webApp _ =
choose [
route "/person"
>=> tryBindQuery<Adult> parsingError None adultHandler
RequestErrors.NOT_FOUND "Not found"
]
If an application has only one model to deal with then this is fairly straight forward, but if an application has more models which require additional data validation steps like in the case of Adult
then you'll quickly end up writing a lot of boilerplate code. This can be avoided with the help of IModelValidation<'T>
and validateModel<'T>
:
[<CLIMutable>]
type Adult =
{
FirstName : string
MiddleName : string option
LastName : string
Age : int
}
override this.ToString() =
sprintf "Name: %s%s %s, Age: %i"
this.FirstName
(if this.MiddleName.IsSome then " " + this.MiddleName.Value else "")
this.LastName
this.Age
member this.HasErrors() =
if this.FirstName.Length < 3 then Some "First name is too short."
else if this.FirstName.Length > 50 then Some "First name is too long."
else if this.LastName.Length < 3 then Some "Last name is too short."
else if this.LastName.Length > 50 then Some "Last name is too long."
else 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.badRequest (text msg))
| None -> Ok this
module WebApp =
let textHandler (x : obj) = text (x.ToString())
let parsingError err = RequestErrors.BAD_REQUEST err
let webApp _ =
choose [
route Urls.person
>=> tryBindQuery<Adult> parsingError None (validateModel textHandler)
]
Now the Adult
type has implemented the IModelValidation<'T>
interface from where it was able to re-use the already existing HasErrors
method to either return a validated object of type Adult
or an error of type HttpHandler
.
The validateModel
method has now been added between the tryBindQuery<Adult>
and textHandler
functions, which means it will validate the model using its IModelValidation<Adult>.Validate()
method.
On success the textHandler
will be executed as normal and on error it will invoke the error handler returned from Validate()
.
ASP.NET Core makes it really easy to process uploaded files.
The HttpContext.Request.Form.Files
collection can be used to process one or many small files which have been sent by a client:
open Giraffe
let fileUploadHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
return!
(match ctx.Request.HasFormContentType with
| false -> RequestErrors.BAD_REQUEST "Bad request"
| true ->
ctx.Request.Form.Files
|> Seq.fold (fun acc file -> sprintf "%s\n%s" acc file.FileName) ""
|> text) next ctx
}
let webApp = route "/upload" >=> fileUploadHandler
You can also read uploaded files by utilizing the IFormFeature
and the ReadFormAsync
method:
let fileUploadHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let formFeature = ctx.Features.Get<IFormFeature>()
let! form = formFeature.ReadFormAsync CancellationToken.None
return!
(form.Files
|> Seq.fold (fun acc file -> sprintf "%s\n%s" acc file.FileName) ""
|> text) next ctx
}
let webApp = route "/upload" >=> fileUploadHandler
For large file uploads it is recommended to stream the file in order to prevent resource exhaustion.
See also large file uploads in ASP.NET Core on StackOverflow.
ASP.NET Core has a wealth of Authentication and Authorization options which work out of the box with Giraffe.
Additionally Giraffe offers a few HttpHandler
functions which make it easier to work with ASP.NET Core's authentication and authorization APIs in a functional way.
The requiresAuthentication (authFailedHandler : HttpHandler)
http handler validates if a user has been authenticated by one of ASP.NET Core's authentication middleware. If the identity of a user could not be established then the authFailedHandler
will be executed:
let notLoggedIn =
RequestErrors.UNAUTHORIZED
"Basic"
"Some Realm"
"You must be logged in."
let mustBeLoggedIn = requiresAuthentication notLoggedIn
let webApp =
choose [
route "/" >=> text "Hello World"
route "/user" >=>
mustBeLoggedIn >=>
choose [
GET >=> readUserHandler
POST >=> submitUserHandler
]
]
The requiresRole (role : string) (authFailedHandler : HttpHandler)
http handler checks if an authenticated user is part of a given role
. If a user fails to be in a certain role then the authFailedHandler
will be executed:
let notLoggedIn =
RequestErrors.UNAUTHORIZED
"Basic"
"Some Realm"
"You must be logged in."
let notAdmin =
RequestErrors.FORBIDDEN
"Permission denied. You must be an admin."
let mustBeLoggedIn = requiresAuthentication notLoggedIn
let mustBeAdmin = requiresRole "Admin" notAdmin
let webApp =
choose [
route "/" >=> text "Hello World"
route "/user" >=>
mustBeLoggedIn >=> mustBeAdmin >=>
choose [
routef "/user/%s/edit" editUserHandler
routef "/user/%s/delete" deleteUserHandler
]
]
The requiresRoleOf (roles : string list) (authFailedHandler : HttpHandler)
http handler checks if an authenticated user is part of a list of given roles
. If a user fails to be in at least one of the roles
then the authFailedHandler
will be executed:
let notLoggedIn =
RequestErrors.UNAUTHORIZED
"Basic"
"Some Realm"
"You must be logged in."
let notProUserOrAdmin =
RequestErrors.FORBIDDEN
"Permission denied. You must be a pro user or admin."
let mustBeLoggedIn = requiresAuthentication notLoggedIn
let mustBeProUserOrAdmin =
requiresRoleOf [ "ProUser"; "Admin" ] notProUserOrAdmin
let webApp =
choose [
route "/" >=> text "Hello World"
route "/user" >=>
mustBeLoggedIn >=> mustBeProUserOrAdmin >=>
choose [
routef "/user/%s/edit" editUserHandler
routef "/user/%s/delete" deleteUserHandler
]
]
The authorizeRequest (predicate : HttpContext -> bool) (authFailedHandler : HttpHandler)
http handler validates a request based on a given predicate. If the predicate returns false then the authFailedHandler
will get executed:
let apiKey = "some-secret-key-1234"
let validateApiKey (ctx : HttpContext) =
match ctx.TryGetRequestHeader "X-API-Key" with
| Some key -> apiKey.Equals key
| None -> false
let accessDenied = setStatusCode 401 >=> text "Access Denied"
let requiresApiKey =
authorizeRequest validateApiKey accessDenied
let webApp =
choose [
route "/" >=> text "Hello World"
route "/private"
>=> requiresApiKey
>=> protectedResource
]
The authorizeUser (policy : ClaimsPrincipal -> bool) (authFailedHandler : HttpHandler)
http handler checks if an authenticated user meets a given user policy. If the policy cannot be satisfied then the authFailedHandler
will get executed:
let notLoggedIn =
RequestErrors.UNAUTHORIZED
"Basic"
"Some Realm"
"You must be logged in."
let accessDenied = setStatusCode 401 >=> text "Access Denied"
let mustBeLoggedIn = requiresAuthentication notLoggedIn
let mustBeJohn =
authorizeUser (fun u -> u.HasClaim (ClaimTypes.Name, "John")) accessDenied
let webApp =
choose [
route "/" >=> text "Hello World"
route "/john-only"
>=> mustBeLoggedIn
>=> mustBeJohn
>=> userHandler
]
The authorizeByPolicyName (policyName : string) (authFailedHandler : HttpHandler)
http handler checks if an authenticated user meets a given authorization policy. If the policy cannot be satisfied then the authFailedHandler
will get executed:
let notLoggedIn =
RequestErrors.UNAUTHORIZED
"Basic"
"Some Realm"
"You must be logged in."
let accessDenied = setStatusCode 401 >=> text "Access Denied"
let mustBeLoggedIn = requiresAuthentication notLoggedIn
let mustBeOver21 =
authorizeByPolicyName "MustBeOver21" accessDenied
let webApp =
choose [
route "/" >=> text "Hello World"
route "/adults-only"
>=> mustBeLoggedIn
>=> mustBeOver21
>=> userHandler
]
The authorizeByPolicy (policy : AuthorizationPolicy) (authFailedHandler : HttpHandler)
http handler checks if an authenticated user meets a given authorization policy. If the policy cannot be satisfied then the authFailedHandler
will get executed.
See authorizeByPolicyName for more information.
The challenge (authScheme : string)
http handler will challenge the client to authenticate with a specific authScheme
. This function is often used in combination with the requiresAuthentication
http handler:
let webApp =
choose [
route "/" >=> text "Hello World"
route "/user" >=>
requiresAuthentication (challenge "Cookie") >=>
choose [
GET >=> readUserHandler
POST >=> submitUserHandler
]
]
In this example the client will be challenged to authenticate with a scheme called "Cookie". The scheme name must match one of the registered authentication schemes from the configuration of the ASP.NET Core auth middleware.
The signOut (authScheme : string)
http handler will sign a user out from a given authScheme
:
let logout = signOut "Cookie" >=> redirectTo false "/"
let webApp =
choose [
route "/" >=> text "Hello World"
route "/user" >=>
requiresAuthentication (challenge "Cookie") >=>
choose [
GET >=> readUserHandler
POST >=> submitUserHandler
route "/user/logout" >=> logout
]
]
Conditional HTTP headers (e.g. If-Match
, If-Modified-Since
, etc.) are a common pattern to improve performance (web caching), to combat the lost update problem or to perform optimistic concurrency control when a client requests a resource from a web server.
Giraffe offers the validatePreconditions
http handler which can be used to run HTTP pre-validation checks against a given ETag
and/or Last-Modified
value of an incoming HTTP request:
let someHandler (eTag : string)
(lastModified : DateTimeOffset)
(content : string) =
let eTagHeader = Some (EntityTagHeaderValue.FromString true eTag)
validatePreconditions eTagHeader (Some lastModified)
>=> setBodyFromString content
The validatePreconditions
handler takes in two optional parameters - an eTag
and a lastMofified
date time value - which will be used to validate a conditional HTTP request. If all conditions can be met, or if no conditions have been submitted, then the next
http handler (of the Giraffe pipeline) will get invoked. Otherwise, if one of the pre-conditions fails or if the resource hasn't changed since the last check, then a 412 Precondition Failed
or a 304 Not Modified
response will get returned.
The ETag (Entity Tag) value is an opaque identifier assigned by a web server to a specific version of a resource found at a URL. The Last-Modified value provides a timestamp indicating the date and time at which the origin server believes the selected representation was last modified.
Giraffe's validatePreconditions
http handler validates the following conditional HTTP headers:
If-Match
If-None-Match
If-Modified-Since
If-Unmodified-Since
The If-Range
HTTP header will not get validated as part the validatePreconditions
http handler, because it is a streaming specific check which gets handled by Giraffe's Streaming functionality.
Alternatively Giraffe exposes the HttpContext
extension method ValidatePreconditions (eTag) (lastModified)
which can be used to create a custom conditional http handler. The ValidatePreconditions
method takes the same two optional parameters and returns a result of type Precondition
.
The Precondition
union type contains the following cases:
Case | Description and Recommended Action |
---|---|
NoConditionsSpecified |
No validation has taken place, because the client didn't send any conditional HTTP headers. Proceed with web request as normal. |
ConditionFailed |
At least one condition couldn't be satisfied. It is advised to return a 412 status code back to the client (you can use the HttpContext.PreconditionFailedResponse() method for that purpose). |
ResourceNotModified |
The resource hasn't changed since the last visit. The server can skip processing this request and return a 304 status code back to the client (you can use the HttpContext.NotModifiedResponse() method for that purpose). |
AllConditionsMet |
All pre-conditions were satisfied. The server should continue processing the request as normal. |
The validatePreconditions
http handler as well as the ValidatePreconditions
extension method will not only validate all conditional HTTP headers, but also set the required ETag
and/or Last-Modified
HTTP response headers according to the HTTP spec.
Both functions follow latest HTTP guidelines and validate all conditional headers in the correct precedence as defined in RFC 2616.
Example of HttpContext.ValidatePreconditions
:
// Pass an optional eTag and lastModified timestamp into the handler, because generating an eTag might require to load the entire resource into memory and therefore this is not something which should be done on every request.
let someHttpHandler eTag lastModified : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
match ctx.ValidatePreconditions eTag lastModified with
| ConditionFailed -> return ctx.PreconditionFailedResponse()
| ResourceNotModified -> return ctx.NotModifiedResponse()
| AllConditionsMet | NoConditionsSpecified ->
// Continue as normal
// Do stuff
}
let webApp =
choose [
route "/" >=> text "Hello World"
route "/foo" >=> someHttpHandler
]
With this feature, we can add guards or limitations to the kind of requests that reach the server. Requests with a certain value for the Accept
, Content-Type
or Content-Length
headers can be checked for acceptable values and a configurable user-friendly error message is send back to the consumer automatically when the conditions are not met.
In order to configure this response, you must use a record type named OptionalErrorHandlers
:
// the type definition
type OptionalErrorHandlers =
{ InvalidHeaderValue: HttpHandler option
HeaderNotFound: HttpHandler option }
// to use the default handlers
let optionalErrorHandlers =
{ InvalidHeaderValue = None; HeaderNotFound = None }
As shown at the previous code block, you can simply use None
for the record and use our default handlers, which will change the response status code to 406 (not acceptable), and return a piece of text to the client explaining what happened.
For now, the helper middlewares we offer are:
Accept
Guards http request based on its Accept
header:
// Only allow http requests with an `Accept` header equals `application/json`.
let webApp =
GET >=> mustAcceptAny [ "application/json" ] optionalErrorHandlers >=> text "Hello World"
// Http request with `Accept` = `application/json` -> Pass through
// Http request without `Accept` = `application/json -> Error status code 406.
// If you define your custom error handler, we use them, otherwise will return one of the following text messages:
// 1) Request rejected because 'Accept' header was not found
// 2) Request rejected because 'Accept' header hasn't got expected MIME type
Content-Type
Guards http request based on its Content-Type
header:
// Only allow http request with a `Content-Type` header `equals `application/json`.
let webApp =
GET >=> hasAnyContentTypes "application/json" optionalErrorHandlers >=> text "Hello World"
// Http request with `Content-Type` = `application/json` -> Pass through
// Http request without `Content-Type` = `application/json` -> Error status code 406.
// If you define your custom error handler, we use them, otherwise will return one of the following text messages:
// 1) Request rejected because 'Content-Type' header was not found
// 2) Request rejected because 'Content-Type' header hasn't got expected value
- Note: with
hasAnyContentTypes
multipleContent-Type
headers can be passed to verify if the http request has any of the provided header values.
Content-Length
Guards http request based on its Content-Length
header:
// Only allow http request with a `Content-Length` header less than or equal than provided maximum bytes.
let webApp =
GET >=> maxContentLength 100L >=> text "Hello World"
// Http request with `Content-Length` = `45` -> Pass through
// Http request without `Content-Length` = `3042` -> Error status code 406.
// If you define your custom error handler, we use them, otherwise will return one of the following text messages:
// 1) Request rejected because there is no 'Content-Length' header
// 2) Request rejected because 'Content-Length' header is too large
Sending a response back to a client in Giraffe can be done through a small range of HttpContext
extension methods and their equivalent HttpHandler
functions.
The WriteBytesAsync (bytes : byte[])
extension method and the setBody (bytes : byte array)
http handler both write a byte array
to the response stream of the HTTP request:
let someHandler (bytes : byte array) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
// Do stuff
return! ctx.WriteBytesAsync bytes
}
// or...
let someHandler (bytes : byte array) : HttpHandler =
// Do stuff
setBody bytes
Both functions will also set the Content-Length
HTTP header to the length of the byte array
.
The WriteStringAsync (str : string)
extension method and the setBodyFromString (str : string)
http handler are both small helper functions which UTF8
decode the string
into a byte array
and subsequently write the byte array
to the response stream of the HTTP request.
Both functions will also set the Content-Length
HTTP header to the correct length of the response:
let someHandler (str : string) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
// Do stuff
return! ctx.WriteStringAsync str
}
// or...
let someHandler (str : string) : HttpHandler =
// Do stuff
setBodyFromString str
The setBody
and setBodyFromString
http handlers (and their HttpContext
extension method equivalents) are useful when you want to create your own response writing function for a specific media type which is not provided by Giraffe yet.
For example Giraffe doesn't have any functionality for serializing and writing a YAML response back to a client. However, you can reference another third party library which can serialize an object into a YAML string and then create your own yaml
http handler like this:
let yaml (x : obj) : HttpHandler =
setHttpHeader "Content-Type" "text/yaml"
>=> setBodyFromString (YamlSerializer.toYaml x)
The WriteTextAsync (str : string)
extension method and the text (str : string)
http handler are the same as writing strings except that they will also set the Content-Type
HTTP header to text/plain
in the response:
let someHandler (str : string) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
// Do stuff
return! ctx.WriteTextAsync str
}
// or...
let someHandler (str : string) : HttpHandler =
// Do stuff
text str
The WriteJsonAsync<'T> (dataObj : 'T)
extension method and the json<'T> (dataObj : 'T)
http handler will both serialize an object to a JSON string and write the output to the response stream of the HTTP request. They will also set the Content-Length
HTTP header and the Content-Type
header to application/json
in the response:
let someHandler (animal : Animal) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
// Do stuff
return! ctx.WriteJsonAsync animal
}
// or...
let someHandler (animal : Animal) : HttpHandler =
// Do stuff
json animal
The underlying JSON serializer can be configured as a dependency during application startup (see JSON).
The WriteJsonChunkedAsync<'T> (dataObj : 'T)
extension method and the jsonChunked (dataObj : 'T)
http handler write directly to the response stream of the HTTP request without extra buffering into a byte array. They will not set a Content-Length
header and instead set the Transfer-Encoding: chunked
header and Content-Type: application/json
:
let someHandler (person : Person) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
// Do stuff
return! ctx.WriteJsonChunkedAsync person
}
// or...
let someHandler (person : Person) : HttpHandler =
// Do stuff
jsonChunked person
The WriteXmlAsync (dataObj : obj)
extension method and the xml (dataObj : obj)
http handler will both serialize an object to an XML string and write the output to the response stream of the HTTP request. They will also set the Content-Length
HTTP header and the Content-Type
header to application/xml
in the response:
let someHandler (dataObj : obj) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
// Do stuff
return! ctx.WriteXmlAsync dataObj
}
// or...
let someHandler (dataObj : obj) : HttpHandler =
// Do stuff
xml dataObj
The underlying XML serializer can be configured as a dependency during application startup (see XML).
The WriteHtmlFileAsync (filePath : string)
extension method and the htmlFile (filePath : string)
http handler will both read a file from the local file system and write the content to the response stream of the HTTP request. They will also set the Content-Length
HTTP header and the Content-Type
header to text/html
:
let someHandler (dataObj : obj) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
// Do stuff
return! ctx.WriteHtmlFileAsync "index.html"
}
// or...
let someHandler (dataObj : obj) : HttpHandler =
// Do stuff
htmlFile "index.html"
Both functions accept either a relative or an absolute path to the HTML file.
The WriteHtmlStringAsync (html : string)
extension method and the htmlString (html : string)
http handler are both equivalent to writing strings except that they will also set the Content-Type
header to text/html
:
let someHandler (dataObj : obj) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
// Do stuff
return! ctx.WriteHtmlStringAsync "<html><head></head><body>Hello World</body></html>"
}
// or...
let someHandler (dataObj : obj) : HttpHandler =
// Do stuff
htmlString "<html><head></head><body>Hello World</body></html>"
Giraffe comes with its own extremely powerful view engine for functional developers (see Giraffe View Engine). The WriteHtmlViewAsync (htmlView : XmlNode)
extension method and the htmlView (htmlView : XmlNode)
http handler will both compile a given html view into valid HTML code and write the output to the response stream of the HTTP request. Additionally they will both set the Content-Length
HTTP header to the correct value and set the Content-Type
header to text/html
:
let indexView =
html [] [
head [] [
title [] [ str "Giraffe" ]
]
body [] [
h1 [] [ str "Giraffe" ]
p [] [ str "Hello World." ]
]
]
let someHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
// Do stuff
return! ctx.WriteHtmlViewAsync indexView
}
// or...
let someHandler : HttpHandler =
// Do stuff
htmlView indexView
Giraffe's default response writers will always send a response in a specific media type regardless of a client's own requirements. Content negotiation on the other hand allows a Giraffe web server to examine a web request's Accept
HTTP header and decide an appropriate data representation on the fly.
The NegotiateAsync (responseObj : obj)
extension method and the negotiate (responseObj : obj)
http handler will both pick the most appropriate data representation based on a request's Accept
HTTP header and write a data object to the response stream of an HTTP request:
[<CLIMutable>]
type Person =
{
FirstName : string
LastName : string
}
let johnDoe =
{
FirstName = "John"
LastName = "Doe"
}
let someHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
// Do stuff
return! ctx.NegotiateAsync johnDoe
}
// or...
let webApp =
choose [
route "/foo" >=> negotiate johnDoe
route "/bar" >=> someHandler
]
Currently Giraffe only supports plain text, JSON and XML responses during content negotiation out of the box. If a client doesn't accept any of these media types then the default negotiation function will return a 406 Unacceptable
HTTP response.
The default negotiation behaviour can be customized by creating a new class which implements the INegotiationConfig
interface and set up a new dependency of that type during application startup.
The INegotiationConfig
has two members which must be implemented:
Rules
of typeIDictionary<string, obj -> HttpHandler>
UnacceptableHandler
of typeHttpHandler
The Rules
property is of type IDictionary<string, obj -> HttpHandler>
and represents a key/value dictionary, where the key denotes a supported Content-Type
and the value represents a function which turns a given obj
into an HttpHandler
.
For example the rules of the DefaultNegotiationConfig
are as following:
dict [
"*/*" , json
"application/json", json
"application/xml" , xml
"text/xml" , xml
"text/plain" , fun x -> x.ToString() |> text
]
In addition to the DefaultNegotiationConfig
, there is also a JsonOnlyNegotiationConfig
provided which only returns JSON.
As you can see from the example above the default dictionary uses the json
and xml
http handlers to define the response handler for the respective media types. If a client requests a text/plain
response then a new function had to be created which accepts an obj
and uses the .ToString()
method in combination with the text
http handler to return a plain text response.
If a client has no particular preference (*/*
) then the default response is json
.
The UnacceptableHandler
is an http handler which will be invoked if none of the client's accepted media types are supported by the web server and therefore the request cannot be satisfied.
Let's assume you have created your own bson
http handler which can serialize an object into BSON and write the contents to the response stream of the request:
let bson (o : obj) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
// Implement BSON handler here
In order for negotiate
and NegotiateAsync
to support the new bson
http handler we need to create a new type which implements INegotiationConfig
:
type CustomNegotiationConfig (baseConfig : INegotiationConfig) =
let plainText x = text (x.ToString())
interface INegotiationConfig with
member __.UnacceptableHandler =
baseConfig.UnacceptableHandler
member __.Rules =
dict [
"*/*" , json
"application/json", json
"application/xml" , xml
"text/xml" , xml
"application/bson", bson
"text/plain" , plainText
]
Then register an instance of the newly created class during application startup:
let configureServices (services : IServiceCollection) =
// First register all default Giraffe dependencies
services.AddGiraffe() |> ignore
// Now register your custom INegotiationConfig
services.AddSingleton<INegotiationConfig>(
CustomNegotiationConfig(
DefaultNegotiationConfig())
) |> ignore
[<EntryPoint>]
let main _ =
WebHost.CreateDefaultBuilder()
.Configure(Action<IApplicationBuilder> configureApp)
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
.Build()
.Run()
0
In this example the CustomNegotiationConfig
uses composition to re-use the UnacceptableHandler
from the DefaultNegotiationConfig
without having to use inheritance.
Alternatively you can also use the NegotiateWithAsync
extension method or the negotiateWith
http handler to configure content negotiation through partial function application:
let customNegotiationRules =
dict [
"*/*" , json
"application/json", json
"application/xml" , xml
"text/xml" , xml
"application/bson", bson
"text/plain" , plainText
]
let customUnacceptableHandler =
setStatusCode 406
>=> text "Request cannot be satisfied by the web server."
// Override the default negotiate handler with a new custom implementation
let negotiate =
negotiateWith
customNegotiationRules
customUnacceptableHandler
Sometimes a large file or block of data has to be send to a client and in order to avoid loading the entire data into memory a Giraffe web application can use streaming to send a response in a more efficient way.
The WriteStreamAsync
extension method and the streamData
http handler can be used to stream an object of type Stream
to a client.
Both functions accept the following parameters:
enableRangeProcessing
: If true a client can request a sub range of data to be streamed (useful when a client wants to continue streaming after a paused download, or when internet connection has been lost, etc.)stream
: The stream object to be returned to the client.eTag
: Entity header tag used for conditional requests (see Conditional Requests).lastModified
: Last modified timestamp used for conditional requests (see Conditional Requests).
If the eTag
or lastModified
timestamp are set then both functions will also set the ETag
and/or Last-Modified
HTTP headers during the response:
let someStream : Stream = ...
let someHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
// Do stuff
return! ctx.WriteStreamAsync(
true, // enableRangeProcessing
someStream,
None, // eTag
None) // lastModified
}
// or...
let someHandler : HttpHandler =
// Do stuff
streamData
true // enableRangeProcessing
someStream
None // eTag
None // lastModified
In most cases a web application will want to stream a file directly from the local file system. In this case you can use the WriteFileStreamAsync
extension method or the streamFile
http handler, which are both the same as WriteStreamAsync
and streamData
except that they accept a relative or absolute filePath
instead of a Stream
object:
let someHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
// Do stuff
return! ctx.WriteFileStreamAsync(
true, // enableRangeProcessing
"large-file.zip",
None, // eTag
None) // lastModified
}
// or...
let someHandler : HttpHandler =
// Do stuff
streamFile
true // enableRangeProcessing
"large-file.zip"
None // eTag
None // lastModified
All streaming functions in Giraffe will also validate conditional HTTP headers, including the If-Range
HTTP header if enableRangeProcessing
has been set to true
.
The redirectTo (permanent : bool) (location : string)
http handler can be used to redirect a client to a different location when handling an incoming web request:
let webApp =
choose [
route "/new" >=> text "Hello World"
route "/old" >=> redirectTo true "https://myserver.com/new"
]
Please note that if the permanent
flag is set to true
then the Giraffe web application will send a 301
HTTP status code to browsers which will tell them that the redirection is permanent. This often leads to browsers cache the information and not hit the deprecated URL a second time any more. If this is not desired then please set permanent
to false
in order to guarantee that browsers will continue hitting the old URL before redirecting to the (temporary) new one.
ASP.NET Core comes with a standard Response Caching Middleware which works out of the box with Giraffe.
If you are not already using one of the two ASP.NET Core meta packages (Microsoft.AspNetCore.App
or Microsoft.AspNetCore.All
) then you will have to add an additional reference to the Microsoft.AspNetCore.ResponseCaching NuGet package.
After adding the NuGet package you need to register the response caching middleware inside your application's startup code before registering Giraffe:
let configureServices (services : IServiceCollection) =
services
.AddResponseCaching() // <-- Here the order doesn't matter
.AddGiraffe() // This is just registering dependencies
|> ignore
let configureApp (app : IApplicationBuilder) =
app.UseGiraffeErrorHandler(errorHandler)
.UseStaticFiles() // Optional if you use static files
.UseAuthentication() // Optional if you use authentication
.UseResponseCaching() // <-- Before UseGiraffe webApp
.UseGiraffe webApp
After setting up the ASP.NET Core response caching middleware you can use Giraffe's response caching http handlers to add response caching to your routes:
// A test handler which generates a new GUID on every request
let generateGuidHandler : HttpHandler =
warbler (fun _ -> text (Guid.NewGuid().ToString()))
let webApp =
GET >=> choose [
route "/route1" >=> publicResponseCaching 30 None >=> generateGuidHandler
route "/route2" >=> noResponseCaching >=> generateGuidHandler
]
Requests to /route1
can be cached for up to 30 seconds whilst requests to /route2
have response caching completely disabled.
Note: if you test the above code with Postman then make sure you disable the No-Cache feature in Postman in order to test the correct caching behaviour.
Giraffe offers in total 4 http handlers which can be used to configure response caching for an endpoint.
In the above example we used the noResponseCaching
http handler to completely disable response caching on the client and on any proxy server. The noResponseCaching
http handler will send the following HTTP headers in the response:
Cache-Control: no-store, no-cache
Pragma: no-cache
Expires: -1
The publicResponseCaching
or privateResponseCaching
http handlers will enable response caching on the client and/or on proxy servers. The
publicResponseCaching
http handler will set the Cache-Control
directive to public
, which means that not only the client is allowed to cache a response for the given cache duration, but also any intermediary proxy server as well as the ASP.NET Core middleware. This is useful for HTTP GET/HEAD endpoints which do not hold any user specific data, authentication data or any cookies and where the response data doesn't change frequently.
The privateResponseCaching
http handler sets the Cache-Control
directive to private
which means that only the end client is allowed to store the response for the given cache duration. Proxy servers and the ASP.NET Core response caching middleware must not cache the response.
Both http handlers require the cache duration in seconds and an optional vary
parameter:
// Cache for 10 seconds without any vary headers
publicResponseCaching 10 None
// Cache for 30 seconds with Accept and Accept-Encoding as vary headers
publicResponseCaching 30 (Some "Accept, Accept-Encoding")
The vary
parameter specifies which HTTP request headers must be respected to vary the cached response. For example if an endpoint returns a different response (Content-Type
) based on the client's Accept
header (= content negotiation) then the Accept
header must also be considered when returning a response from the cache. The same applies if the web server has response compression enabled. If a response varies based on the client's accepted compression algorithm then the cache must also respect the client's Accept-Encoding
HTTP header when serving a response from the cache.
The ASP.NET Core response caching middleware offers one more additional feature which is not part of the response's HTTP headers. By default, if a route is cacheable then the middleware will try to return a cached response even if the query parameters were different.
For example if a request to /foo/bar
has been cached, then the cached version will also be returned if a request is made to /foo/bar?query1=a
or /foo/bar?query1=a&query2=b
.
Sometimes this is not desired and the VaryByQueryKeys
feature lets the middleware vary its cached responses based on a request's query keys.
The generic responseCaching
http handler is the most basic response caching handler which can be used to configure custom response caching handlers as well as make use of the VaryByQueryKeys
feature:
responseCaching
(Public (TimeSpan.FromSeconds (float 5)))
(Some "Accept, Accept-Encoding")
(Some [| "query1"; "query2" |])
The first parameter is of type CacheDirective
which is defines as following:
type CacheDirective =
| NoCache
| Public of TimeSpan
| Private of TimeSpan
The second parameter is an string option
which defines the vary
parameter.
The third and last parameter is a string list option
which defines an optional list of query parameter values which must be used to vary a cached response by the ASP.NET Core response caching middleware. Please be aware that this feature only applies to the ASP.NET Core response caching middleware and will not be respected by any intermediate proxy servers.
ASP.NET Core has its own Response Compression Middleware which works out of the box with Giraffe. There's no additional functionality or http handlers required in order to make it work with Giraffe web applications.
Giraffe has its own functional view engine which can be used to build rich UIs for web applications. The single biggest and best contrast to other view engines (e.g. Razor, Liquid, etc.) is that the Giraffe View Engine is entirely functional, written in normal (and compiled) F# code.
This means that the Giraffe View Engine is by definition one of the most feature rich view engines available, requires no disk IO to load a view and views are automatically compiled at build time.
The Giraffe View Engine uses traditional functions and F# record types to generate rich HTML/XML views.
Please visit the Giraffe.ViewEngine project page to learn more about it!
By default Giraffe uses System.Text.Json
for (de-)serializing JSON content. An application can modify the default serializer by registering a new dependency which implements the Json.ISerializer
interface during application startup.
It's possible to use a serializer compatible with Fsharp types: Json.FsharpFriendlySerializer
instead of Json.Serializer
(C#-like). This uses FSharp.SystemTextJson
to customize System.Text.Json
.
You can change the entire underlying JSON serializer by creating a new class which implements the Json.ISerializer
interface:
type CustomJsonSerializer() =
interface Json.ISerializer with
// Use different JSON library ...
member __.SerializeToString<'T> (x : 'T) = // ...
member __.SerializeToBytes<'T> (x : 'T) = // ...
member __.SerializeToStreamAsync<'T> (x : 'T) = // ...
member __.Deserialize<'T> (json : string) = // ...
member __.Deserialize<'T> (bytes : byte[]) = // ...
member __.DeserializeAsync<'T> (stream : Stream) = // ...
For example, one could define a Newtonsoft.Json
serializer:
[<RequireQualifiedAccess>]
module NewtonsoftJson =
open System.IO
open System.Text
open System.Threading.Tasks
open Microsoft.IO
open Newtonsoft.Json
open Newtonsoft.Json.Serialization
type Serializer (settings : JsonSerializerSettings, rmsManager : RecyclableMemoryStreamManager) =
let serializer = JsonSerializer.Create settings
let utf8EncodingWithoutBom = UTF8Encoding(false)
static member DefaultSettings =
JsonSerializerSettings(
ContractResolver = CamelCasePropertyNamesContractResolver())
interface Json.ISerializer with
member __.SerializeToString (x : 'T) =
JsonConvert.SerializeObject(x, settings)
member __.SerializeToBytes (x : 'T) =
JsonConvert.SerializeObject(x, settings)
|> Encoding.UTF8.GetBytes
member __.SerializeToStreamAsync (x : 'T) (stream : Stream) =
task {
use memoryStream = rmsManager.GetStream("giraffe-json-serialize-to-stream")
use streamWriter = new StreamWriter(memoryStream, utf8EncodingWithoutBom)
use jsonTextWriter = new JsonTextWriter(streamWriter)
serializer.Serialize(jsonTextWriter, x)
jsonTextWriter.Flush()
memoryStream.Seek(0L, SeekOrigin.Begin) |> ignore
do! memoryStream.CopyToAsync(stream, 65536)
} :> Task
member __.Deserialize<'T> (json : string) =
JsonConvert.DeserializeObject<'T>(json, settings)
member __.Deserialize<'T> (bytes : byte array) =
let json = Encoding.UTF8.GetString bytes
JsonConvert.DeserializeObject<'T>(json, settings)
member __.DeserializeAsync<'T> (stream : Stream) =
task {
use memoryStream = rmsManager.GetStream("giraffe-json-deserialize")
do! stream.CopyToAsync(memoryStream)
memoryStream.Seek(0L, SeekOrigin.Begin) |> ignore
use streamReader = new StreamReader(memoryStream)
use jsonTextReader = new JsonTextReader(streamReader)
return serializer.Deserialize<'T>(jsonTextReader)
}
Then register a new instance of the newly created type during application startup:
let configureServices (services : IServiceCollection) =
// First register all default Giraffe dependencies
services.AddGiraffe() |> ignore
// Now register your custom Json.ISerializer
services.AddSingleton<Json.ISerializer>(fun serviceProvider ->
NewtonsoftJson.Serializer(JsonSerializerSettings(), serviceProvider.GetService<Microsoft.IO.RecyclableMemoryStreamManager>()) :> Json.ISerializer) |> ignore
[<EntryPoint>]
let main _ =
WebHost.CreateDefaultBuilder()
.Configure(Action<IApplicationBuilder> configureApp)
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
.Build()
.Run()
0
Check this samples/NewtonsoftJson project to find this code in a working program.
You can change the default JsonSerializerSettings
of a JSON serializer by registering a new instance of Json.ISerializer
during application startup. For example, the Microsoft.FSharpLu
project provides a Newtonsoft JSON converter (CompactUnionJsonConverter
) that serializes and deserializes Option
s and discriminated unions much more succinctly. If you wanted to use it, and set the culture to German, your configuration would look something like:
let configureServices (services : IServiceCollection) =
// First register all default Giraffe dependencies
services.AddGiraffe() |> ignore
// Now customize only the Json.ISerializer by providing a custom
// object of JsonSerializerSettings
let customSettings = JsonSerializerSettings(
Culture = CultureInfo("de-DE"))
customSettings.Converters.Add(CompactUnionJsonConverter(true))
services.AddSingleton<Json.ISerializer>(
NewtonsoftJson.Serializer(customSettings)) |> ignore
[<EntryPoint>]
let main _ =
WebHost.CreateDefaultBuilder()
.Configure(Action<IApplicationBuilder> configureApp)
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
.Build()
.Run()
0
If you need you retrieve the registered JSON serializer from a custom HttpHandler
function then you can do this with the GetJsonSerializer
extension method:
let customHandler (dataObj : obj) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
let serializer = ctx.GetJsonSerializer()
let json = serializer.Serialize dataObj
// ... do more...
By default Giraffe uses the System.Xml.Serialization.XmlSerializer
for (de-)serializing XML content. An application can modify the serializer by registering a new dependency which implements the Xml.ISerializer
interface during application startup.
Customizing Giraffe's XML serialization can either happen via providing a custom object of XmlWriterSettings
when instantiating the default SystemXml.Serializer
or swap in an entire different XML library by creating a new class which implements the Xml.ISerializer
interface.
You can change the default XmlWriterSettings
of the SystemXml.Serializer
by registering a new instance of SystemXml.Serializer
during application startup:
let configureServices (services : IServiceCollection) =
// First register all default Giraffe dependencies
services.AddGiraffe() |> ignore
// Now customize the Xml.ISerializer
let customSettings =
XmlWriterSettings(
Encoding = Encoding.UTF8,
Indent = false,
OmitXmlDeclaration = true
)
services.AddSingleton<Xml.ISerializer>(
SystemXml.Serializer(customSettings)) |> ignore
[<EntryPoint>]
let main _ =
WebHost.CreateDefaultBuilder()
.Configure(Action<IApplicationBuilder> configureApp)
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
.Build()
.Run()
0
You can change the entire underlying XML serializer by creating a new class which implements the Xml.ISerializer
interface:
type CustomXmlSerializer() =
interface Xml.ISerializer with
// Use different XML library ...
member __.Serialize (o : obj) = // ...
member __.Deserialize<'T> (xml : string) = // ...
Then register a new instance of the newly created type during application startup:
let configureServices (services : IServiceCollection) =
// First register all default Giraffe dependencies
services.AddGiraffe() |> ignore
// Now register your custom Xml.ISerializer
services.AddSingleton<Xml.ISerializer, CustomXmlSerializer>() |> ignore
[<EntryPoint>]
let main _ =
WebHost.CreateDefaultBuilder()
.Configure(Action<IApplicationBuilder> configureApp)
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
.Build()
.Run()
0
If you need you retrieve the registered XML serializer from a custom HttpHandler
function then you can do this with the GetXmlSerializer
extension method:
let customHandler (dataObj : obj) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
let serializer = ctx.GetXmlSerializer()
let xml = serializer.Serialize dataObj
// ... do more...
Testing a Giraffe application follows the concept of ASP.NET Core testing.
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.TestHost
open Microsoft.AspNetCore.Hosting
open System.Net.Http
let getTestHost() =
WebHostBuilder()
.UseTestServer()
.Configure(Action<IApplicationBuilder> [YourApp].configureApp)
.ConfigureServices([YourApp].configureServices)
.ConfigureLogging([YourApp].configureLogging)
.UseUrls([YourUrl])
let testRequest (request : HttpRequestMessage) =
let resp = task {
use server = new TestServer(getTestHost())
use client = server.CreateClient()
let! response = request |> client.SendAsync
return response
}
resp.Result
// Import needed for the code below:
open System.Net
[<Fact>]
let ``Hello world endpoint says hello`` () =
let response = testRequest (new HttpRequestMessage(HttpMethod.Get, "/hello-world"))
let content = response.Content.ReadAsStringAsync().Result
Assert.Equal(response.StatusCode, HttpStatusCode.OK)
Assert.Equal(content, "hello")
[<Fact>]
let ``Example HTTP Post`` () =
let request = new HttpRequestMessage(HttpMethod.Post, "/hello-world")
request.Content <- "{\"JsonField\":\"JsonValue\"}"
let response = testRequest request
Assert.Equal(response.StatusCode, HttpStatusCode.OK)
// Check the json content
On top of default HTTP related functions such as HttpContext
extension methods and HttpHandler
functions Giraffe also provides a few other helper functions which are commonly required in Giraffe web applications.
The ShortGuid
and ShortId
modules offer helper functions to work with Short GUIDs and Short IDs inside Giraffe.
The ShortGuid.fromGuid
function will convert a System.Guid
into a URL friendly 22 character long string
value.
The ShortGuid.toGuid
function will convert a 22 character short GUID string
into a valid System.Guid
object. This function can be useful when converting a string
query parameter into a valid Guid
argument:
let someHttpHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
let guid =
match ctx.TryGetQueryStringValue "id" with
| None -> Guid.Empty
| Some shortGuid -> ShortGuid.toGuid shortGuid
// Do something with `guid`...
// Return a Task<HttpContext option>
The ShortId.fromUInt64
function will convert an uint64
into a URL friendly 11 character long string
value.
The ShortId.toUInt64
function will convert a 11 character short ID string
into a uint64
value. This function can be useful when converting a string
query parameter into a valid uint64
argument:
let someHttpHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
let id =
match ctx.TryGetQueryStringValue "id" with
| None -> 0UL
| Some shortId -> ShortId.toUInt64 shortId
// Do something with `id`...
// Return a Task<HttpContext option>
Short GUIDs and short IDs can also be automatically resolved from route arguments.
The GetRequestUrl
extension method of the HttpContext
type can be used to retrieve the entire URL of the HTTP request as a string
value:
let someHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
let requestUrl = ctx.GetRequestUrl()
text (sprintf "The request URL is: %s" requestUrl) next ctx
Giraffe automatically adds two extensions methods to the DateTime
and DateTimeOffset
objects. ToIsoString()
formats a given timestamp into an RFC3339 formatted string and ToHtmlString()
which formats the given timestamp into an RFC822 formatted string:
let now = DateTimeOffset.UtcNow
let isoFormattedTimestamp = now.ToIsoString()
let now = DateTimeOffset.UtcNow
let htmlFormattedTimestamp = now.ToHtmlString()
The F# language provides an isNull
function for checking null
values when interoping with other .NET languages. Unfortunately there is no isNotNull
function by default. Giraffe closes that gap by providing an additional isNotNull
function:
if isNotNull someObj then
// ... do stuff here
else
// ... do other stuff here
An F# application often has to check if a string
value is null
when interoping with other .NET languages. Representing an optionally missing value with null
is unnatural in F# and therefore Giraffe provides the strOption
function which can convert a string
into an Option<string>
value for a more natural F# experience. If a string is null
then the strOptoin
function will return None
, otherwise Some string
:
let someDateTime =
match strOption someString with
| Some str -> DateTimeOffset.Parse str
| None -> DateTimeOffset.UtcNow
Reading a file from the local file system is often a common use case in a web application. The readFileAsStringAsync
function will asynchronously read the entire content of a given filePath
from the local file system:
let someFunction =
task {
let! content = readFileAsStringAsync "myfile.txt"
// ... do stuff
}
Giraffe provides two additional computation expressions which can be used with Option<'T>
and Result<'T, 'TError>
objects.
The opt {}
computation expression can be used to bind options and the res {}
computation expression can be used to bind result objects:
open Giraffe.ComputationExpressions
let someHttpHandler : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
let result =
res {
let! header1 = ctx.GetRequestHeader "X-Header-1"
let! header2 = ctx.GetRequestHeader "X-Header-2"
let! header3 = ctx.GetRequestHeader "X-Header-3"
return (header1, header2, header3)
}
match result with
| Ok (h1, h2, h3) ->
sprintf "%s, %s, %s" h1 h2 h3
|> ctx.WriteTextAsync
| Error msg -> RequestErrors.BAD_REQUEST msg next ctx
There's more features available for Giraffe web applications through additional NuGet packages:
Starting with Giraffe 5.x we introduced a new module called Giraffe.EndpointRouting
. The endpoint routing module implements an alternative router to Giraffe's default routing functions which integrates with ASP.NET Core's endpoint routing APIs.
Given the way how ASP.NET Core's Endpoint Routing works this module comes with several benefits (and unfortunately also some minor downsides) in comparison to Giraffe's default router. The main benefit of Giraffe.EndpointRouting
is that it nicely integrates with the rest of ASP.NET Core and can benefit from everything which Endpoint Routing makes possible. It also means that any performance improvements made to the ASP.NET Core router will directly translate to Giraffe. The downsides are that several existing routing functions couldn't be ported to Giraffe.EndpointRouting
and routes are case-insensitive by default. Whilst this can be a problem with some applications overall the limitations are minimal and the benefits should greatly outweigh the downsides in the long term. Endpoint Routing is definitely the new preferred option of routing in ASP.NET Core and will undoubtedly see a lot of investment and improvements by the ASP.NET team over the years.
At last it is possible to have the Giraffe.EndpointRouting
module and Giraffe's default router work side by side, benefiting from Endpoint Routing where possible and keeping the default router elsewhere.
Notice that the usage of Giraffe.EndpointRouting
is recommended, as described in this issue.
In order to make use of Giraffe's endpoint routing functions one has to open the required module first:
open Giraffe.EndpointRouting
Giraffe's HTTP handlers remain unchanged regardless if they are used from a typical Giraffe router or the Giraffe.EndpointRouting
module. This makes porting to the Giraffe.EndpointRouting
module tremendously easy:
let handler1 : HttpHandler =
fun (_ : HttpFunc) (ctx : HttpContext) ->
ctx.WriteTextAsync "Hello World"
Unlike Giraffe's default router (which really is just a big HttpHandler
function often implemented with the help of the choose
function) the endpoint router requires a flat list of Endpoint
functions:
let endpoints =
[
GET [
route "/" (text "Hello World")
routef "/%s/%i" handler2
routef "/%s/%s/%s/%i" handler3
]
subRoute "/sub" [
// Not specifying a http verb means it will listen to all verbs
route "/test" handler1
]
]
Then the Endpoint list
must be initialised with ASP.NET Core's EndpointMiddleware
instead of being passed into the GiraffeMiddleware
:
let configureApp (appBuilder : IApplicationBuilder) =
appBuilder
.UseRouting()
.UseEndpoints(fun e -> e.MapGiraffeEndpoints(endpoints))
|> ignore
The main differences are:
- Additionally to
HttpHandler
functions there is a new type calledEndpoint
- The router is a flat list of
Endpoint
functions - The
GET
,POST
,route
, etc. functions map a conventionalHttpHandler
to anEndpoint
function (when theGiraffe.EndpointRouting
module has been opened) - The final
Endpoint list
has to be passed into ASP.NET Core'sEndpointMiddleware
instead of using theGiraffeMiddleware
The MapGiraffeEndpoints
extension method translates those functions into the final RequestDelegate
functions which the EndpointMiddleware
relies on and therefore the Giraffe.EndpointRouting
module doesn't add any extra overhead or runtime cost to ASP.NET Core's endpoint routing resolution.
The following routing functions are available as part of the Giraffe.EndpointRouting
module:
GET
,POST
,PUT
,PATCH
,DELETE
,HEAD
,OPTIONS
,TRACE
,CONNECT
route
routef
subRoute
The route
, routef
and subRoute
handlers are all case-insensitive. Other handlers such as routex
, subRoutef
or choose
are not supported by the Giraffe.EndpointRouting
module.
The choose
handler is replaced by composing an Endpoint list
.
Other routing handlers couldn't be ported like for like, but the ASP.NET Core Endpoint Routing API allows for greater control and better insight into an endpoint by exposing useful helper functions.
Using the GetRouteData
extension method one can get access to route values and data tokens from within a handler:
open Microsoft.AspNetCore.Routing
let myHandler (foo : int, bar : string) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
let routeData = ctx.GetRouteData()
routeData.Values // Values produced on the current path
routeData.DataTokens // Tokens produced on the current path
sprintf "Yada Yada %i %s" foo bar
|> ctx.WriteTextAsync
The GetEndpoint
extension method returns the endpoint for the currently executed path and can be used to further explore the metadata and other data attached to this endpoint:
let myHandler (foo : int, bar : string) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
let endpoint = ctx.GetEndpoint()
For more information about ASP.NET Core Endpoint Routing please refer to the official documentation.
The Giraffe.TokenRouter
NuGet package exposes an alternative routing HttpHandler
which is based on top of a Radix Tree. Several routing handlers (e.g.: routef
and subRoute
) have been overridden in such a way that path matching and value parsing are significantly faster than using the basic choose
function.
This implementation assumes that additional memory and compilation time is not an issue. If speed and performance of parsing and path matching is required then the Giraffe.TokenRouter
can be a much better fit.
Please check the official Giraffe TokenRouter GitHub repository for more information.
The Giraffe.Razor
NuGet package adds fully featured Razor support to Giraffe web applications.
For more information please visit the official Giraffe Razor GitHub repository.
The Giraffe.DotLiquid
NuGet package adds DotLiquid support to Giraffe web applications.
For more information please visit the official Giraffe DotLiquid GitHub repository.
The Giraffe.OpenApi
NuGet package, inspired by Oxpecker's code, adds OpenAPI support to Giraffe, helping to improve the documentation of the project.
For more information please visit the official Giraffe OpenApi GitHub repository.
Saturn is an opinionated, web development framework built on top of Giraffe which implements the server-side, functional MVC pattern for F#.
Saturn is not directly part of Giraffe but builds a Phoenix inspired MVC pattern on top of Giraffe. It is being developed and maintained by the author of the Ionide project.
[<AutoOpen>]
module Giraffe.Query
open Aether
open Microsoft.AspNetCore.Http
module Helpers =
let konst v _ = v
[<RequireQualifiedAccess>]
module Option =
let inline ofBool b = if b then Some [] else None
open Helpers
[<AutoOpen>]
module Values =
type QueryValue = string list option
[<RequireQualifiedAccess>]
module QueryValue =
let inline private (|Empty|NonEmpty|) xs =
match xs with
| [] -> Empty
| _ -> NonEmpty xs
(* Epimorphisms *)
let private Zero__ =
(function | None -> Some ()
| _ -> None), konst None
let private Bool__ =
(function | None -> Some false
| Some Empty -> Some true
| _ -> None), Option.ofBool
let private String__ =
(function | Some [v] -> Some v
| _ -> None), List.singleton >> Some
let private List__ =
(function | None -> Some []
| Some vs -> Some vs), Some
(* Prisms *)
let Zero_ =
Prism.ofEpimorphism Zero__
let Bool_ =
Prism.ofEpimorphism Bool__
let String_ =
Prism.ofEpimorphism String__
let List_ =
Prism.ofEpimorphism List__
(* Functional *)
[<AutoOpen>]
module Functional =
type QueryValueResult<'a> = Result<'a, string>
type QueryValue<'a> = QueryValue -> QueryValueResult<'a> * QueryValue
(* Functions *)
[<RequireQualifiedAccess>]
module QueryValue =
let inline unit (a: 'a) : QueryValue<_> =
fun value ->
Ok a, value
let zero = unit ()
let inline error (e: string) : QueryValue<_> =
fun value ->
Error e, value
let inline internal ofResult result =
fun value ->
result, value
let inline bind (m: QueryValue<'a>) (f: 'a -> QueryValue<'b>) : QueryValue<'b> =
fun value ->
match m value with
| Ok a, value -> f a value
| Error e, value -> Error e, value
let inline apply (f: QueryValue<'a -> 'b>) (m: QueryValue<'a>) : QueryValue<'b> =
bind f (fun f' ->
bind m (f' >> unit))
let inline map (f: 'a -> 'b) (m: QueryValue<'a>) : QueryValue<'b> =
bind m (f >> unit)
let inline map2 (f: 'a -> 'b -> 'c) (m1: QueryValue<'a>) (m2: QueryValue<'b>) : QueryValue<'c> =
apply (apply (unit f) m1) m2
(* Operators *)
module Operators =
let inline (>>=) m f =
QueryValue.bind m f
let inline (=<<) f m =
QueryValue.bind m f
let inline (<*>) f m =
QueryValue.apply f m
let inline (<!>) f m =
QueryValue.map f m
let inline ( *>) m1 m2 =
QueryValue.map2 (konst id) m1 m2
let inline (<* ) m1 m2 =
QueryValue.map2 konst m1 m2
let inline (>=>) f g =
fun x -> f x >>= g
let inline (<=<) g f =
fun x -> f x >>= g
module Builder =
open Operators
type QueryValueBuilder () =
member inline __.Bind (m1, f) = m1 >>= f
member inline __.Combine (m1, m2) = m1 *> m2
member inline __.Delay f = QueryValue.zero >>= f
member inline __.Return x = QueryValue.unit x
member inline __.Zero () = QueryValue.zero
let queryValue = Builder.QueryValueBuilder ()
[<AutoOpen>]
module Optic =
[<RequireQualifiedAccess>]
module QueryValue =
[<RequireQualifiedAccess>]
module Optic =
type Get =
| Get with
static member (^.) (Get, l: Lens<QueryValue, 'b>) : QueryValue<_> =
fun value ->
Ok (Optic.get l value), value
static member (^.) (Get, p: Prism<QueryValue, 'b>) : QueryValue<_> =
fun value ->
match Optic.get p value with
| Some x -> Ok x, value
| None -> Error (sprintf "Couldn't use Prism %A on query string value: '%A'" p value), value
let inline get o : QueryValue<_> =
(Get ^. o)
type TryGet =
| TryGet with
static member (^.) (TryGet, l: Lens<QueryValue, 'b>) : QueryValue<_> =
fun value ->
Ok (Some (Optic.get l value)), value
static member (^.) (TryGet, p: Prism<QueryValue, 'b>) : QueryValue<_> =
fun value ->
Ok (Optic.get p value), value
let inline tryGet o : QueryValue<_> =
(TryGet ^. o)
let inline set o v : QueryValue<_> =
fun query ->
Ok (), Optic.set o v query
let inline map o f : QueryValue<_> =
fun query ->
Ok (), Optic.map o f query
[<AutoOpen>]
module Mapping =
open Operators
(* From *)
(* Defaults *)
type FromQueryValueDefaults = FromQueryValueDefaults with
(* Basic Types *)
static member inline FromQueryValue (_: unit) =
QueryValue.Optic.get QueryValue.Zero_
static member inline FromQueryValue (_: bool) =
QueryValue.Optic.get QueryValue.Bool_
static member inline FromQueryValue (_: string) =
QueryValue.Optic.get QueryValue.String_
static member inline FromQueryValue (_: QueryValue) =
QueryValue.Optic.get id_
(* Mapping Functions *)
let inline internal fromQueryValueDefaults (a: ^a, _: ^b) =
((^a or ^b) : (static member FromQueryValue: ^a -> ^a QueryValue) a)
let inline internal fromQueryValue x =
fst (fromQueryValueDefaults (Unchecked.defaultof<'a>, FromQueryValueDefaults) x)
let inline internal fromQueryValueFold xs =
List.fold (fun r x ->
match r with
| Error e -> Error e
| Ok xs ->
match fromQueryValue x with
| Ok x -> Ok (x :: xs)
| Error e -> Error e) (Ok []) (xs |> List.map (List.singleton >> Some) |> List.rev)
let inline private tryParse name f =
fun value ->
match f value with
| true, v -> fun value -> Ok v, value
| _ -> fun value -> Error (sprintf "Failed to parse '%A' as %s" value name), value
(* Defaults *)
open System
type FromQueryValueDefaults with
(* Numbers *)
static member inline FromQueryValue (_: float) =
tryParse "float" Double.TryParse
=<< QueryValue.Optic.get QueryValue.String_
static member inline FromQueryValue (_: decimal) =
tryParse "decimal" Decimal.TryParse
=<< QueryValue.Optic.get QueryValue.String_
static member inline FromQueryValue (_: int) =
tryParse "int" Int32.TryParse
=<< QueryValue.Optic.get QueryValue.String_
static member inline FromQueryValue (_: int16) =
tryParse "int16" Int16.TryParse
=<< QueryValue.Optic.get QueryValue.String_
static member inline FromQueryValue (_: int64) =
tryParse "int64" Int64.TryParse
=<< QueryValue.Optic.get QueryValue.String_
static member inline FromQueryValue (_: float32) =
tryParse "float32" Single.TryParse
=<< QueryValue.Optic.get QueryValue.String_
static member inline FromQueryValue (_: uint16) =
tryParse "uint16" UInt16.TryParse
=<< QueryValue.Optic.get QueryValue.String_
static member inline FromQueryValue (_: uint32) =
tryParse "uint32" UInt32.TryParse
=<< QueryValue.Optic.get QueryValue.String_
static member inline FromQueryValue (_: uint64) =
tryParse "uint64" UInt64.TryParse
=<< QueryValue.Optic.get QueryValue.String_
(* Lists *)
static member inline FromQueryValue (_: 'a list) : QueryValue<'a list> =
fromQueryValueFold >> QueryValue.ofResult
=<< QueryValue.Optic.get QueryValue.List_
static member inline FromQueryValue (_: 'a array) : QueryValue<'a array> =
fromQueryValueFold >> Result.map Array.ofList >> QueryValue.ofResult
=<< QueryValue.Optic.get QueryValue.List_
(* Set *)
static member inline FromQueryValue (_: Set<'a>) : QueryValue<Set<'a>> =
fromQueryValueFold >> Result.map Set.ofList >> QueryValue.ofResult
=<< QueryValue.Optic.get QueryValue.List_
(* Options *)
static member inline FromQueryValue (_: 'a option) : QueryValue<'a option> =
fun value ->
match fromQueryValue value with
| Ok v -> Ok (Some v), value
| _ -> Ok None, value
type Query = Map<string, string list>
module Convert =
open Microsoft.AspNetCore.WebUtilities
let toQuery (qs: QueryString) : Query =
QueryHelpers.ParseQuery qs.Value
|> Seq.map (fun kvp -> kvp.Key, kvp.Value |> List.ofSeq)
|> Map.ofSeq
(* Functional *)
[<AutoOpen>]
module Functional =
type QueryResult<'a> = Result<'a, string>
type Query<'a> = Query -> QueryResult<'a> * Query
(* Functions *)
[<RequireQualifiedAccess>]
module Query =
let inline unit x : Query<_> =
fun query ->
Ok x, query
let zero = unit ()
let inline error e : Query<_> =
fun query ->
Error e, query
let inline internal ofResult result =
fun query ->
result, query
let inline bind (m: Query<'a>) (f: 'a -> Query<'b>) : Query<'b> =
fun query ->
match m query with
| Ok a, query -> f a query
| Error e, query -> Error e, query
let inline apply f m =
bind f (fun f' ->
bind m (f' >> unit))
let inline map f m =
bind m (f >> unit)
let inline map2 f m1 m2 =
apply (apply (unit f) m1) m2
(* Operators *)
module Operators =
let inline (>>=) m f =
Query.bind m f
let inline (=<<) f m =
Query.bind m f
let inline (<*>) f m =
Query.apply f m
let inline (<!>) f m =
Query.map f m
let inline ( *>) m1 m2 =
Query.map2 (konst id) m1 m2
let inline (<* ) m1 m2 =
Query.map2 konst m1 m2
let inline (>=>) f g =
fun x -> f x >>= g
let inline (<=<) g f =
fun x -> f x >>= g
(* Builder *)
module Builder =
open Operators
type QueryBuilder () =
member inline __.Bind (m1, f) = m1 >>= f
member inline __.Combine (m1, m2) = m1 *> m2
member inline __.Delay f = Query.zero >>= f
member inline __.Return x = Query.unit x
member inline __.Zero () = Query.zero
let query = Builder.QueryBuilder ()
[<AutoOpen>]
module Mapping =
open Operators
(* From *)
(* Defaults *)
type FromQueryDefaults = FromQueryDefaults with
static member inline FromQuery (_: Query) : Query<Query> =
fun query -> Ok query, query
static member inline FromQuery (_: Map<string, string>) : Query<Map<string, string>> =
fun query ->
let ret =
query
|> Map.filter (konst (function | [_] -> true | _ -> false))
|> Map.map (konst List.head)
Ok ret, query
(* Mapping Functions *)
let inline internal fromQueryDefaults (a: ^a, _: ^b) =
((^a or ^b) : (static member FromQuery: ^a -> ^a Query) a)
let inline internal fromQuery x =
fst (fromQueryDefaults (Unchecked.defaultof<'a>, FromQueryDefaults) x)
(* Functions *)
[<RequireQualifiedAccess>]
module Query =
(* Read *)
let private readValue key =
fun query ->
Ok (Map.tryFind key query), query
let readMemberWith fromQueryValue key =
readValue key
>>= fun value ->
match fromQueryValue value with
| Ok v -> Query.unit v
| Error e -> Query.error (sprintf "%s: %s" key e)
let inline readWith fromQueryValue key =
readMemberWith fromQueryValue key
let inline read key =
readWith fromQueryValue key
let inline parse qs =
fromQuery (Convert.toQuery qs)
|> function | Ok a -> a
| Error e -> failwith e
let inline tryParse qs =
fromQuery (Convert.toQuery qs)
|> function | Ok a -> Some a
| Error _ -> None
[<AutoOpen>]
module HttpHandlers =
open Giraffe.HttpHandlers
open System.Threading.Tasks
module Query =
let inline bind (f: ^a -> HttpHandler) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
match Query.tryParse ctx.Request.QueryString with
| None -> return None
| Some a -> return! f a next ctx
}
let inline bindTask (f: ^a -> Task<HttpHandler>) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
match Query.tryParse ctx.Request.QueryString with
| None -> return None
| Some a ->
let! handler = f a
return! handler next ctx
}