FSharp.Data.GraphQL


Working with GraphQL type system

A central point of each GraphQL service is its schema. Schema defines a namespace for type definitions exposed to service consumers, that can be used later to generate things such as documentation, introspection queries and finally to execute and validate incoming GraphQL queries and mutations.

Each schema definition requires to have something called root query - it's a GraphQL object definition, which exposes all top level fields to be requested by the user. Aside of it there are also separate roots for mutations (queries are readonly requests) and subscriptions (which is an experimental feature, at the moment of v0.2-beta is not yet supported by Fsharp.Data.GraphQL).

GraphQL type system categorizes several custom types, that can be defined by programmer, including:

Beside them, FSharp.Data.GraphQL defines two others:

  • Lists - which can be used to compose types defined above in context of collections.
  • Nullables - which can be used to define potentially absent fields in form of the F# option types. This differs from GraphQL standard, where fields are nullable by default and can be optionally marked as non-null. Such approach however wouldn't fit the spirit of FSharp programming language.

One of the important distintions, that can save your time in the future is difference between Input and Output type definitions:

  1. Output types define valid types that can be produced as GraphQL query response. This includes:
    • Objects
    • Interfaces
    • Unions
    • Enums
    • Scalars
    • Lists
    • Nullables
  2. Input types define types, which can be used as valid values included in GraphQL query itself or as set of attached variables. This includes:
    • InputObjects
    • Enums
    • Scalars
    • Lists
    • Nullables

Please, note the distinction between those two - not all type definitions are both valid inputs and outputs.

Defining an Object

Objects are the most common GraphQL type definitions. They describe a complex value with a set of well-defined fields, i.e:

1: 
2: 
3: 
let Person = Define.Object("Person", [
    Define.Field("lastName", String, fun _ person -> person.LastName)
    Define.AsyncField("picture", Uri, fun _ person -> getPictureUrl(person.Id)) ])

You can enhance most of the GraphQL components with description (this includes both object and fields definitions) - it later can be queried and used by tools like graphiql to be used as documentation.

Fields can also be parametrized - by specifying a list of arguments in GraphQL field definition you can use them later to pass runtime parameters, that may differ with each query, even when query structure remains the same. Do define an argument use Define.Input helper method:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
let pageSize = 20
Define.Field("people", ListOf Person, "Single page", [ Define.Input("page", Int) ], fun ctx db ->
    let page = ctx.Arg "page"
    query {
        for person in db.People do
        sortBy person.Id
        skip (page * pageSize)
        take pageSize
        select person
    } |> Seq.toList) 

Defining recursive type references

Sometimes you may find hard to define a GraphQL types having recursive relationship to each other. Good example of that would be a Person object with field friends returning list of instances of type Person itself. It's hard to do so due to limitations of F# language. However you can still achieve that by using overloaded Define.Object method:

1: 
2: 
3: 
let rec Person = Define.Object(name = "Person", fieldsFn = fun () -> [
    // ... some person fields
    Define.Field("friends", ListOf Person, fun _ p -> p.Friends) ])

As you may see, we defined Person object definition using rec keyword and instead of defining fields as a list and we used a lazily evaluated function instead.

Defining an Interface

GraphQL interfaces are so called abstract types (along with unions). This means, that they can be used as part of the query, however query materialization must always be bound to some concrete Object type definition.

Object definitions must explicitly implement interfaces and all of their fields in order to use them:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
let rec Animal = Define.Interface<obj>("Animal", [
    Define.Field("name", String) ])

and Cat = Define.Object<Cat>(
    name = "Cat",
    interfaces = [Animal],
    fields = [
        Define.Field("name", String, fun _ cat -> cat.Name)
        Define.Field("meows", Boolean, fun _ cat -> cat.Meows) ])

and Dog = Define.Object<Dog>(
    name = "Dog",
    interfaces = [Animal],
    fields = [
        Define.Field("name", String, fun _ dog -> dog.Name)
        Define.Field("barks", Boolean, fun _ dog -> dog.Barks) ])

What's worth noticing here is that GraphQL interface definitions are not necessarily bound to .NET interfaces. You can create an interface definition (like the one above) which will be known only to GraphQL schema.

Once you've defined a schema, you can retrieve all definitions implementing target interface, by calling schema.GetPossibleTypes(Animal).

Defining an Union

GraphQL unions are basically enumerations of one of the object definitions. Unlike F# discriminated unions, they work on existing object types, that can be used on their own. This makes them initially heavy to work with.

However with FSharp.Data.GraphQL, it is possible combine existing F# types with discriminated unions under the GraphQL union definition:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
type Cat = { Name: string; Meows: bool }
type Dog = { Name: string; Barks: bool }
type Pet = 
    | DogPet of Dog
    | CatPet of Cat

let Cat = Define.Object<Cat>("Cat", [
        Define.Field("name", String, fun _ cat -> cat.Name)
        Define.Field("meows", Boolean, fun _ cat -> cat.Meows) ])

let Dog = Define.Object<Dog>("Dog", [
        Define.Field("name", String, fun _ dog -> dog.Name)
        Define.Field("barks", Boolean, fun _ dog -> dog.Barks) ])

let Pet = Define.Union<Pet>(
    name = "Pet",
    options = [ Cat; Dog ],
    resolveType = function DogPet _ -> upcast Dog | CatPet _ -> upcast Cat,
    resolveValue = function DogPet d -> box d | CatPet c -> upcast c)

The example above shows, how you can use Pet discriminated union as a proxy to being able to define GraphQL union type and still being able to operate with .NET type system in safe manner.

Defining an Enum

GraphQL enums can be quite closelly related to C# enums - they define primitive types (used as GraphQL leaf types) that have strictly defined set of possible values, i.e:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
type Ord =
    | Gt = 1
    | Eq = 0
    | Lt = -1

let Ord = Define.Enum("Ord", [
    Define.EnumValue("Lesser", Ord.Lt)
    Define.EnumValue("Equal", Ord.Eq)
    Define.EnumValue("Greater", Ord.Greater) ])

The major difference from .NET here is that GraphQL expects, that enum values are serialized as strings. Therefore, upon serialization, given enum value will be projected using ToString() method.

Defining a Scalar

Just like enums, GraphQL scalars are leaf types, that should be able to be serialized/deserialized into primitive types supported by format of your choice (which will be JSON in most of the cases).

1: 
2: 
3: 
4: 
let Guid = Define.Scalar(
    name = "Guid",
    coerceInput = (fun (StringValue s) -> match Guid.TryParse(s) with true, g -> Some g | false, _ -> None),
    coerceValue = fun v -> match v with | :? Guid g -> Some g | _ -> None)

This examples shows how to create a scalar definition for .NET Guid type. It requires two functions to be defines:

  1. coerceInput function, which will be used to resolve your scalar value directly from values encoded in GraphQL query string (in this case StringValue is just part of parsed query AST).
  2. coerceValue function, which will be used to apply something like "implicit casts" between two .NET types in conflicting cases - like when value is passed in variables query string part - which should be tolerated.

Defining an Input Object

Just like objects, input objects describe a complex data types - however while Objects are designed to work as output types, Input Objects are input types only.

1: 
2: 
3: 
4: 
type CreateAccountData = { Email: string; Password: string }
let CreateAccountData = Define.InputObject("CreateAccountData", [
    Define.Input("email", String)
    Define.Input("password", String) ])

Unlike the objects, you neither define input object field resolver nor provide any arguments for it. They also don't work together with abstract types like interfaces or unions.

val Person : obj

Full name: typesystem.Person
module String

from Microsoft.FSharp.Core
val pageSize : int

Full name: typesystem.pageSize
val query : Linq.QueryBuilder

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.query
module Seq

from Microsoft.FSharp.Collections
val toList : source:seq<'T> -> 'T list

Full name: Microsoft.FSharp.Collections.Seq.toList
val Animal : obj

Full name: typesystem.Animal
Multiple items
type InterfaceAttribute =
  inherit Attribute
  new : unit -> InterfaceAttribute

Full name: Microsoft.FSharp.Core.InterfaceAttribute

--------------------
new : unit -> InterfaceAttribute
type obj = System.Object

Full name: Microsoft.FSharp.Core.obj
val Cat : obj

Full name: typesystem.Cat
val Dog : obj

Full name: typesystem.Dog
Multiple items
val Cat : obj

Full name: typesystem.Cat

--------------------
type Cat =
  {Name: string;
   Meows: bool;}

Full name: typesystem.Cat
Cat.Name: string
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = System.String

Full name: Microsoft.FSharp.Core.string
Cat.Meows: bool
type bool = System.Boolean

Full name: Microsoft.FSharp.Core.bool
Multiple items
val Dog : obj

Full name: typesystem.Dog

--------------------
type Dog =
  {Name: string;
   Barks: bool;}

Full name: typesystem.Dog
Dog.Name: string
Dog.Barks: bool
type Pet =
  | DogPet of Dog
  | CatPet of Cat

Full name: typesystem.Pet
union case Pet.DogPet: Dog -> Pet
union case Pet.CatPet: Cat -> Pet
Multiple items
val Pet : obj

Full name: typesystem.Pet

--------------------
type Pet =
  | DogPet of Dog
  | CatPet of Cat

Full name: typesystem.Pet
val box : value:'T -> obj

Full name: Microsoft.FSharp.Core.Operators.box
type Ord =
  | Gt = 1
  | Eq = 0
  | Lt = -1

Full name: typesystem.Ord
Ord.Gt: Ord = 1
Ord.Eq: Ord = 0
Ord.Lt: Ord = -1
Multiple items
val Ord : obj

Full name: typesystem.Ord

--------------------
type Ord =
  | Gt = 1
  | Eq = 0
  | Lt = -1

Full name: typesystem.Ord
val Guid : obj

Full name: typesystem.Guid
union case Option.Some: Value: 'T -> Option<'T>
union case Option.None: Option<'T>
type CreateAccountData =
  {Email: string;
   Password: string;}

Full name: typesystem.CreateAccountData
CreateAccountData.Email: string
CreateAccountData.Password: string
Multiple items
val CreateAccountData : obj

Full name: typesystem.CreateAccountData

--------------------
type CreateAccountData =
  {Email: string;
   Password: string;}

Full name: typesystem.CreateAccountData
Fork me on GitHub