Since the beginning of the era of service-based applications and data-oriented APIs, a big struggle is always present not only to reduce traffic and request number on those kind of services, but also to provide a good way for the user to understand how to use it, in the sense of how the information is described and returned.

To help with that, GraphQL uses a type system to define the data that can be queried. It is also database/storage independent, as it is backed by existing code and data that can be modeled in any language or platform.

As a language, it does have a specification that is updated from time to time, and it is evolving fast, always gaining new features and protocols to work with, for example, multipart request for file upload and web sockets for subscriptions. It is open source and maintained by the Facebook team.

Although Facebook only maintains the specification, several languages got implementations for server and client code, helping the user to not having to write a parser and a query runner at all, worrying only about defining the schema and operations. Here, we are going to use FSharp.Data.GraphQL, which is an open-source implementation made for F#, a functional-first language, developed to work inside .NET environment, as C# or VB.NET.

Basic operations

At its simplest, GraphQL is just about asking for specific fields on specific objects. Suppose our data schema is composed by “human” entities, and they have fields called “name” and “height”, we could send a query like this one:

{

  human(id: "1000") {

    name

    height

  }

}

And we should receive a response like this one:

{

  "data": {

    "human": {
    
      "name": "Luke Skywalker",
    
      "height": 1.72
    
    }

  }

}

As we will see, fields and objects inside queries can have things like arguments, directives, and inheritance, so the type system is powerful and allow the user to extract exactly what he needs from the API.

Also, queries are not the only operation supported by GraphQL. We can have things like mutations, which is, as the name says, a special query sent to ask for an update on the server existing data. We can also write subscription queries, which allows the client to be updated whenever something changes on the server, based on the schema implementation.

The Library

One of the strong qualities of FSharp.Data.GraphQL is that, when defining schema and operations, we declare them in a type safe manner. Things like invalid fields or return types will be checked at compile time. It also has support for recently features like live queries and subscriptions – with the flexibility to use built-in implementations or customized ones. On our sample project, we are going to take a little tour on that by making an agenda API: we can use it to set appointments or reminders, and subscribe to them via Web Socket protocol.

The sample project

The sample project we are going to see uses the WebAPI scaffold from .NET, and Giraffe, an open-source project that adds functional support to the ASP.NET Core pipeline. The project can be accessed through the GitHub repository and is called Graphscriber – it is an open source library that adds GraphQL over web socket functionality to the ASP.NET Core pipeline (this repository location may change in the future). Since the server component aims to be agnostic from platform and server architectures, and we want to integrate it to the ASP.NET Core pipeline, we need some boilerplate code, so we are going to use this for it. Basically, the project that shows us how to build a GraphQL server is located in the “Graphscriber.AspNetCore.Tests.WebApp” folder. In the Schema.fs file, we can see how our schema is built. First, we declare our domain types that will be used on our GraphQL schema definition:

type Reminder =

{ Id : Guid

  Subject : string

  Time : DateTime }

  

type Appointment =

{ Id : Guid

  Subject : string

  Location : string

  StartTime : DateTime

  EndTime : DateTime option

  Reminder : Reminder option }

  

type Entry =

| Appointment of Appointment

| Reminder of Reminder

  

type Root =

{ RequestId : Guid }

Then we create a module to control our Storage operations (this is the part were we control persistence):

module Storage =
    let entries = ConcurrentBag<Entry>()

    let private normalize (date : DateTime) =
        DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second)

    let private filterByReminderTime filter (entries : Entry seq) =
        entries
        |> Seq.filter (fun x ->
            match x with
            | Appointment a ->
                match a.Reminder with
                | Some r -> filter r
                | None -> false
            | Reminder r -> filter r)
        |> Seq.sortBy (fun x ->
            match x with
            | Appointment a -> a.Reminder.Value.Time
            | Reminder r -> r.Time)

    let getNextReminders limit =
        entries
        |> filterByReminderTime (fun r -> normalize r.Time > normalize DateTime.Now)
        |> Seq.truncate limit

    let alarmReminders () =
        entries
        |> filterByReminderTime (fun r -> normalize r.Time = normalize DateTime.Now)

    let addReminder subject time =
        let r = { Id = System.Guid.NewGuid(); Subject = subject; Time = time }
        entries.Add(Reminder r); r

    let addAppointment subject location startTime endTime (reminder : DateTime option) =
        let r =
            reminder
            |> Option.map (fun x ->
                { Id = System.Guid.NewGuid()
                Subject = subject
                Time = x })
        let a =
            { Id = System.Guid.NewGuid()
            Subject = subject
            Location = location
            StartTime = startTime
            EndTime = endTime
            Reminder = r }
        entries.Add(Appointment a);

As we can see, the storage provides functions to add reminders and appointments, a function to look for the incoming reminders, and a function to show reminders that should be fired (at DateTime.Now). Of course, we are normalizing our DateTime values to ignore fractions less than seconds, as this will allow us to work in a second basis for our alarm structure.

At last, we declare our schema in the Schema module:

module Schema =
    let ReminderType =
        Define.Object<Reminder>(
            name = "Reminder",
            description = "A reminder in the calendar.",
            isTypeOf = (fun x -> x :? Reminder),
            fieldsFn = fun () ->
                [ Define.Field("id", Guid, "The ID of the reminder.", fun _ (x : Reminder) -> x.Id)
                Define.Field("subject", String, "The subject that the reminder refers to.", fun _ (x : Reminder) -> x.Subject)
                Define.Field("time", Date, "The date and time that the reminder should be fired.", fun _ (x : Reminder) -> x.Time) ])

    let AppointmentType = // …

As we can see, by calling functions of the Define static object, we can create every kind of GraphQL type that we need. Objects and fields can be associated to our domain types in many forms: Object types can use “fieldsFn” parameter to define a function that declare every field of the object, and Field types can be associated to our domain types by the “resolve” parameter. This function is used to produce the value of the field that we want to use.

The last part that we could see is how we are going to subscribe to our agenda reminders. This is done by the Define.SubscriptionObject call:

let SubscriptionType =
        Define.SubscriptionObject<Root>(
            name = "Subscription",
            fields =
                [ Define.SubscriptionField(
                    "incomingReminders", RootType, EntryType, "Subscribes to future reminders (including appointments with reminders).",
                    fun _ _ x -> Some x) ])

The function passed in the last parameter is used as a filter to decide if the value should be sent to subscribers or not. In our case, we are going to send the reminder anyways, so we return always Some. Now we just need to start an asynchronous background loop to look, at every second for reminders that should be sent, and publish them to our subscription provider:

let private config = SchemaConfig.Default

    let instance = Schema(QueryType, MutationType, SubscriptionType, config)

    let executor = Executor(instance)

    do
        async {
            while true do
                Storage.alarmReminders ()
                |> Seq.iter (fun r -> config.SubscriptionProvider.Publish "incomingReminders" r)
                do! Task.Delay(1000) |> Async.AwaitTask
        } |> Async.StartAsTask |> ignore

With that, we have pretty much everything settled, except that we need to provide our WebSocket interface. This component exposes extension methods to IApplicationBuilder to provide that, as we can see in our Startup.cs code. First, we create an healthcheck endpoint just to test if our application is running correctly, then we add Giraffe pipeline as a service, a SocketManager for our root schema object, and we configure those services using extension methods as well:

type Startup() =

    let webApp =
        choose
            [ route "/healthcheck" >=> text "Service is running." ]

    member __.ConfigureServices(services : IServiceCollection) =
        services.AddGiraffe().AddGQLServerSocketManager<Root>() |> ignore

    member __.Configure(app : IApplicationBuilder, _ : IHostingEnvironment) =
        let errorHandler (ex : Exception) (log : ILogger) =
            log.LogError(EventId(), ex, "An unhandled exception has occurred while executing the request.")
            clearResponse >=> setStatusCode 500
        app
            .UseGiraffeErrorHandler(errorHandler)
            .UseGQLWebSockets(Schema.executor, (fun _ -> { RequestId = Guid.NewGuid() }))
            .UseGiraffe(webApp)

This is pretty much all the code that we need to put a F# GraphQL server running with WebSocket protocol. We could use any client to test this, like GraphiQL, which is an interactive client that supports looking into the introspection schema and generating tooltip help while typing the query – or we could just write integration tests using ASP.NET Test Server to simulate a real client application calling each operation through WebSockets. This is demonstrated in the integration test of this project:

test "Should be able to do a mutation (insert a reminder)" {
                let query = """mutation InsertReminder ($subject : String!, $time : Date!) {
                    addReminder (subject : $subject, time : $time) {
                        subject
                        time
                    }
                }"""
                let payload =
                    { Query = query
                    Variables = Map.ofList
                        [ "subject", box "Buy coffee"
                        "time", upcast buyCoffeeDate ] }
                let expectedData =
                    NameValueLookup.ofList
                        [ "addReminder", upcast NameValueLookup.ofList
                            [ "subject", upcast "Buy coffee"
                            "time", upcast buyCoffeeDate ] ]
                connection
                |> sendMessage (Start ("1", payload))
                |> receiveMessage
                |> isSome
                |> isData "1" expectedData
            }