View on GitHub

OttoTheGeek

Ottomatic configuration for GraphQL in C#

Configuring Paging and Ordering

Consider a model that looks like this:

public sealed class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string ModelNumber { get; set; }
    public Manufacturer Manufacturer { get; set; }
    public DateTime FirstRunDate { get; set; }
}
public sealed class Query
{
    public IEnumerable<Product> Products { get; set; }
}
public sealed class Model : OttoModel<Query>
{
    protected override SchemaBuilder ConfigureSchema(SchemaBuilder builder)
    {
        builder.ConnectionField(x => x.Products)
            .ResolvesVia<ProductConnectionResolver>()
            // other things here as well possibly
            ;

    }
}

By configuring Products as a connection field, OttoTheGeek will set up Products as a field that takes arguments specified by the PagingArgs<Product> class (a built-in OttoTheGeek class). This enables your code to return pages of results rather than all the results at once. Resolvers for connection fields return a Connection<T> object and take a PagingArgs<T>. For example:

public sealed class ProductConnectionResolver : IConnectionResolver<Product>
{

    public async Task<Connection<Product>> Resolve(PagingArgs<Product> args)
    {
        // use args.Offset, args.Count, and args.OrderBy to return a page of results
    }
}

The OrderValue<T> Class

One of the properties of PagingArgs<Product> is OrderBy, which is of type OrderValue<Product>. OttoTheGeek configures this as an ENUM type in GraphQL, and uses the properties of the model to determine its values. By default, the enum values of OrderValue<Product> for the model above would be:

If you wanted, say, the first 20 products ordered by ModelNumber, a query might look like:

{
    products(orderBy: ModelNumber_ASC, count: 20, offset: 0) {
        id
        name
        modelNumber
    }
}

When OttoTheGeek calls the Resolve(...) method of ProductConnectionResolver, the value of the OrderBy field will be an OrderValue<Product>. The properties are:

By default, Prop will always be non-null. However, you can customize what sort values are available. Let’s say that we don’t want to sort by Id. Let’s also say that we want to order by manufacturer name. However, the end-user may or may not select that field in their GraphQL query, so we can’t assume that data will be present - we just want to make it possible to order by it:

protected override SchemaBuilder ConfigureSchema(SchemaBuilder builder)
{
    builder.ConnectionField(x => x.Products)
        .ResolvesVia<ProductConnectionResolver>()
        .GraphType<PagingArgs<Product>>(ConfigureProductPagingArgs)
        // other things here as well
        ;

}

private GraphTypeBuilder<PagingArgs<Product>> ConfigureProductPagingArgs(GraphTypeBuilder<PagingArgs<Product>> builder)
{
    return builder.ConfigureOrderBy(
            x => x.OrderBy,
            ConfigureProductOrderBy
            );
}

private OrderByBuilder<Product> ConfigureProductOrderBy(OrderByBuilder<Product> builder)
{
    return builder
        .Ignore(x => x.Id);
        .AddValue("ManufacturerName", descending: false)
        .AddValue("ManufacturerName", descending: true);
}

This will yield the following enum values:

If the user chooses to sort using the manufacturer name, the Prop value of the PagingArgs<Product> will be null, since it doesn’t correspond to a property of the Product class:

public sealed class ProductConnectionResolver : IConnectionResolver<Product>
{

    public async Task<Connection<Product>> Resolve(PagingArgs<Product> args)
    {
        var query = BaseQuery(); // assuming IQueryable<Product>
        if(args.OrderBy.Name == "ManufacturerName")
        {
            // sort differently since this is a custom sort value;
            // args.OrderBy.Prop will be null
        }
        else
        {
            query = query.OrderBy(args.OrderBy);
        }
        // use args.Offset, args.Count to select page
    }
}

As a convenience, OttoTheGeek publishes an extension method on IQueryable<T> that allows you to use an OrderValue<T> to sort an IQueryable<T>, as shown in the example above. This method will throw an exception if using a custom order value since it can’t be automatically translated.

Custom PagingArgs<T>

In some cases, you may need to pass additional arguments for a connection field, such as filtering criteria. In this case, you can define a custom PagingArgs<T> class that contains your additional arguments. For example, let’s say we want to add a search text argument to our Products field. We define our subclass:

public sealed class ProductArgs : PagingArgs<Product>
{
    public string SearchText { get; set; }
}

Then register it when we define our connection field:

builder.ConnectionField(x => x.Products)
    .WithArgs<ProductArgs>()
    .ResolvesVia<ProductConnectionResolver>()

And update our resolver to implement IConnectionResolver<Product, ProductArgs>:

public sealed class ProductConnectionResolver : IConnectionResolver<Product, ProductArgs>
{

    public async Task<Connection<Product>> Resolve(ProductArgs args)
    {
        // implementation here; use args.SearchText as needed
    }