GraphQL with .NET Core (Part - XII: Relay)



Relay is a JavaScript framework for building data-driven React applications. It's a scalable GraphQL client.

To make a Relay compliant GraphQL server, you have to follow this specification. Fortunately, GraphQL-Dotnet community has provided a library to take on the heavy burden.

The current Nuget package for graphql-dotnet/relay doesn't work with the latest version of GraphQL (4.2.2). So, I cloned the GitHub repository and created a Nuget package out of it. The package is available at the root of the sample repository of this blog series.

Install the Nuget package with one of the following commands,

Install-Package GraphQL.Relay -Version 0.6.2

dotnet add package GraphQL.Relay --version 0.6.2

The graphql-dotnet/relay library includes a few GraphQL types and helpers for creating Connections, Mutations, and Nodes. After adding the library, you have to register Relay specific GraphQL types with the ASP.NET Core's dependency injection (DI) container,

services.AddTransient(typeof(ConnectionType<>))
    .AddTransient(typeof(EdgeType<>))
    .AddTransient<NodeInterface>()
    .AddTransient<PageInfoType>();
Startup.cs

The two core assumptions that Relay makes about a GraphQL server are that it provides:

  1. A mechanism for refetching an object.
  2. A description of how to page through connections.

Object Identification

Object identification feature is enabled by using the NodeGraphType. The class extends ObjectGraphType and adds a GetById field, which helps Relay (via the node() Query) to refetch nodes when it needs to. It also provides an Id() for defining a global id for the node. The incoming and outgoing ids are all Base64 encoded.

To make the ItemType a node type, ItemType is extended from AsyncNodeGraphType<Item>,

public class ItemType : AsyncNodeGraphType<Item>
{
    private readonly IRepository _repository;

    public ItemType(IRepository repository)
    {
        _repository = repository;

        Id(p => p.Id);

        Field(i => i.Tag);
        Field(i => i.Title);
        Field(i => i.Price);
    }

    public override async Task<Item> GetById(string id)
    {
        return await _repository.GetItemById(Convert.ToInt32(id));
    }
}
ItemType.cs

Mutations

The library also provides a MutationInputGraphType. It's an abstraction over the regular mutation implemented using Field. The core implementation is as following,

public class MutationGraphType : ObjectGraphType
{
    public MutationGraphType()
    {
        Name = "Mutation";
    }

    public FieldType Mutation<TMutationInput, TMutationType>(string name)
        where TMutationType : IMutationPayload<object>
        where TMutationInput : MutationInputGraphType
    {
        return Field(
            name: name,
            type: typeof(TMutationType),
            arguments: new QueryArguments(
                new QueryArgument<NonNullGraphType<TMutationInput>> { Name = "input" }
            ),
            resolve: c =>
            {
                var inputs = c.GetArgument<Dictionary<string, object>>("input");
                return ((TMutationType)c.FieldDefinition.ResolvedType).MutateAndGetPayload(new MutationInputs(inputs), c);
            }
        );
    }
}
MutationGraphType.cs (graphql-dotnet/relay)

We have to extend the GameStoreMutation from MutationInputGraphType in order to use the Mutation<IMutationPayload<object>, MutationInputGraphType>,

public class GameStoreMutation : MutationGraphType
{
    public GameStoreMutation()
    {
        Mutation<CreateItemInput, CreateItemPayload>("createItem");
    }
}
GameStoreMutation.cs

Followings are the CreateItemInput and CreateItemPayload files,

public class CreateItemInput : MutationInputGraphType
{
    public CreateItemInput()
    {
        Name = "CreateItemInput";
        Field<NonNullGraphType<StringGraphType>>("tag");
        Field<NonNullGraphType<StringGraphType>>("title");
        Field<NonNullGraphType<DecimalGraphType>>("price");
    }
}
CreateItemInput.cs

MutationInputGraphType adds a clientMutationId field on top of InputGraphType. Although clientMutationId  was required for Classic Relay, Modern Relay doesn't need that anymore.

public class CreateItemPayload : MutationPayloadGraphType<object, Task<object>>
{
    private readonly IRepository _repository;

    public CreateItemPayload(IRepository repository)
    {
        _repository = repository;

        Field<ItemType>("item");
    }
    public override async Task<object> MutateAndGetPayload(MutationInputs inputs, IResolveFieldContext<object> context)
    {
        string tag = inputs.Get<string>("tag");
        string title = inputs.Get<string>("title");
        decimal price = inputs.Get<decimal>("price");

        Item item = await _repository.AddItem(new Item { Tag = tag, Title = title, Price = price });

        return new
        {
            item
        };
    }
}
CreateItemPayload.cs

Connections

The pagination and data slicing model used in GraphQL is called the Connection Cursor Specification. The base library graphql-dotnet/graphql-dotnet provides utilities to work with Connection. On top of it,  graphql-dotnet/relay adds additional utilities to make it work with Relay.

To make the items field in the GameStoreQuery a Connection field, modify the code as followings,

Connection<ItemType>()
    .Name("items")
    .PageSize(10)
    .ResolveAsync(async ctx =>
    {
        var loader = accessor.Context.GetOrAddLoader("GetAllItems", repository.GetItems);
        return ConnectionUtils.ToConnection(await loader.LoadAsync().GetResultAsync(), ctx);
    });
GameStoreQuery.cs

Practically, you would want to see a result of a mutation (add/update/delete) directly in your connection. In that case, you should return an EdgeType in your mutation payload. For example, I would want to see the newly added item in my connection, I would modify the CreateItemPayload as following,

public class CreateItemPayload : MutationPayloadGraphType<object, Task<object>>
{
    private readonly IRepository _repository;

    public CreateItemPayload(IRepository repository)
    {
        _repository = repository;

        Field<EdgeType<ItemType>>("itemEdge");
    }
    public override async Task<object> MutateAndGetPayload(MutationInputs inputs, IResolveFieldContext<object> context)
    {
        string tag = inputs.Get<string>("tag");
        string title = inputs.Get<string>("title");
        decimal price = inputs.Get<decimal>("price");

        Item item = await _repository.AddItem(new Item { Tag = tag, Title = title, Price = price });

        return new
        {
            ItemEdge = new Edge<Item>
            {
                Node = item,
                Cursor = ConnectionUtils.CursorForObjectInConnection(await _repository.GetItems(), item)
            },
        };
    }
}
CreateItemPayload.cs

Part-XII

fiyazbinhasan/GraphQLCoreFromScratch
https://fiyazhasan.me/tag/graphql/. Contribute to fiyazbinhasan/GraphQLCoreFromScratch development by creating an account on GitHub.

GraphQL Server Specification

Pagination

GraphQL Cursor Connections Specification