graphql-dotnet / graphql-dotnet/relay
How Can We Simplify Creating Connections
- Dominant language
- C#
- Stars
- 74
- Forks
- 28
- PR merge metrics
- No merged PRs in 30d
Description
In my [.NET Boxed GraphQL project template](https://github.com/Dotnet-Boxed/Templates/blob/master/Docs/GraphQL.md) I have an example of creating a GraphQL connection. I feel like there is too much boilerplate you have to write to get one of these working and I'm wondering if there are some simple ways we can simplify their creation by providing some helper methods.
## Connection
Here is the main code to create the connection on my query graph type:
```
public class QueryObject : ObjectGraphType
{
private const int MaxPageSize = 10;
public QueryObject(IDroidRepository droidRepository)
{
this.Name = "Query";
this.Description = "The query type, represents all of the entry points into our object graph.";
this.Connection()
.Name("droids")
.Description("Gets pages of droids.")
// Enable the last and before arguments to do paging in reverse.
.Bidirectional()
// Set the maximum size of a page, use .ReturnAll() to set no maximum size.
.PageSize(MaxPageSize)
.ResolveAsync(context => ResolveConnection(droidRepository, context));
}
private async static Task ResolveConnection(
IDroidRepository droidRepository,
ResolveConnectionContext context)
{
var first = context.First;
var afterCursor = Cursor.FromCursor(context.After);
var last = context.Last;
var beforeCursor = Cursor.FromCursor(context.Before);
var cancellationToken = context.CancellationToken;
var getDroidsTask = GetDroids(droidRepository, first, afterCursor, last, beforeCursor, cancellationToken);
var getHasNextPageTask = GetHasNextPage(droidRepository, first, afterCursor, cancellationToken);
var getHasPreviousPageTask = GetHasPreviousPage(droidRepository, last, beforeCursor, cancellationToken);
var totalCountTask = droidRepository.GetTotalCount(cancellationToken);
await Task.WhenAll(getDroidsTask, getHasNextPageTask, getHasPreviousPageTask, totalCountTask);
var droids = getDroidsTask.Result;
var hasNextPage = getHasNextPageTask.Result;
var hasPreviousPage = getHasPreviousPageTask.Result;
var totalCount = totalCountTask.Result;
var (firstCursor, lastCursor) = Cursor.GetFirstAndLastCursor(droids, x => x.Created);
return new Connection()
{
Edges = droids
.Select(x =>
new Edge()
{
Cursor = Cursor.ToCursor(x.Created),
Node = x
})
.ToList(),
PageInfo = new PageInfo()
{
HasNextPage = hasNextPage,
HasPreviousPage = hasPreviousPage,
StartCursor = firstCursor,
EndCursor = lastCursor,
},
TotalCount = totalCount,
};
}
private static Task> GetDroids(
IDroidRepository droidRepository,
int? first,
DateTime? afterCursor,
int? last,
DateTime? beforeCursor,
CancellationToken cancellationToken)
{
if (first.HasValue)
return droidRepository.GetDroids(first, afterCursor, cancellationToken);
else
return droidRepository.GetDroidsReverse(last, beforeCursor, cancellationToken);
}
private static async Task GetHasNextPage(
IDroidRepository droidRepository,
int? first,
DateTime? afterCursor,
CancellationToken cancellationToken)
{
if (first.HasValue)
return await droidRepository.GetHasNextPage(first, afterCursor, cancellationToken);
else
return false;
}
private static async Task GetHasPreviousPage(
IDroidRepository droidRepository,
int? last,
DateTime? beforeCursor,
CancellationToken cancellationToken)
{
if (last.HasValue)
return await droidRepository.GetHasPreviousPage(last, beforeCursor, cancellationToken);
else
return false;
}
}
```
## Repository
I feel like I've got too many methods here:
```
public interface IDroidRepository
{
Task> GetDroids(
int? first,
DateTime? createdAfter,
CancellationToken cancellationToken);
Task> GetDroidsReverse(
int? first,
DateTime? createdAfter,
CancellationToken cancellationToken);
Task GetHasNextPage(
int? first,
DateTime? createdAfter,
CancellationToken cancellationToken);
Task GetHasPreviousPage(
int? last,
DateTime? createdBefore,
CancellationToken cancellationToken);
Task GetTotalCount(CancellationToken cancellationToken);
}
```
## Cursors
I created a Cursor helper class to help turn any property of any arbitrary type into an opaque base64 string cursor. The code looks like this:
```
public static class Cursor
{
private const string Prefix = "arrayconnection";
public static T FromCursor(string cursor)
{
if (string.IsNullOrEmpty(cursor))
return default;
string decodedValue;
try
{
decodedValue = Base64Decode(cursor);
}
catch (FormatException)
{
return default;
}
var prefixIndex = Prefix.Length + 1;
if (decodedValue.Length <= prefixIndex)
return default;
var value = decodedValue.Substring(prefixIndex);
return (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture);
}
public static (string firstCursor, string lastCursor) GetFirstAndLastCursor(
IEnumerable enumerable,
Func getCursorProperty)
{
if (getCursorProperty == null)
throw new ArgumentNullException(nameof(getCursorProperty));
if (enumerable == null || enumerable.Count() == 0)
return (null, null);
var firstCursor = ToCursor(getCursorProperty(enumerable.First()));
var lastCursor = ToCursor(getCursorProperty(enumerable.Last()));
return (firstCursor, lastCursor);
}
public static string ToCursor(T value)
{
if (value == null)
throw new ArgumentNullException(nameof(value));
return Base64Encode(string.Format(CultureInfo.InvariantCulture, "{0}:{1}", Prefix, value));
}
private static string Base64Decode(string value) => Encoding.UTF8.GetString(Convert.FromBase64String(value));
private static string Base64Encode(string value) => Convert.ToBase64String(Encoding.UTF8.GetBytes(value));
}
```
I initially raised this as a PR at https://github.com/graphql-dotnet/graphql-dotnet/pull/678 but got confused by @jquense. I'm hoping to pickup that coversation here.
## Ideas
One idea I have is if creating a connection was as simple as implementing one of two interfaces that might looks something like this:
```
this.Connection()
.Name("droids")
.Description("Gets pages of droids.")
.PageSize(MaxPageSize)
.ResolveAsync>();
public interface IBidirectionalConnectionResolver
{
// The property on the model we want to use for the cursor.
TPropertyType GetProperty(Func model);
GetItems(int? first, TPropertyType? after, CancellationToken cancellationToken);
GetItemsReverse(int? last, TPropertyType? before, CancellationToken cancellationToken);
HasNextPage(int? first, TPropertyType? after, CancellationToken cancellationToken);
HasPreviousPage(int? last, TPropertyType? before, CancellationToken cancellationToken);
GetTotalCount(CancellationToken cancellationToken);
}
public interface IConnectionResolver
{
// The same but for a single direction instead of bi-directional.
}
```
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.