From d0382f47dece559aade002e2a5936eef441cc6d9 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 8 May 2020 10:10:33 +0200 Subject: [PATCH 01/15] started work --- .../Types/Types/Relay/IConnectionResolver.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Core/Types/Types/Relay/IConnectionResolver.cs b/src/Core/Types/Types/Relay/IConnectionResolver.cs index de9491cda37..49ae1894292 100644 --- a/src/Core/Types/Types/Relay/IConnectionResolver.cs +++ b/src/Core/Types/Types/Relay/IConnectionResolver.cs @@ -5,6 +5,23 @@ namespace HotChocolate.Types.Relay { public interface IConnectionResolver { - Task ResolveAsync(CancellationToken cancellationToken); + Task ResolveAsync( + object source, + int first, + int last, + string after, + string before, + CancellationToken cancellationToken); + } + + public interface IConnectionResolver : IConnectionResolver + { + Task ResolveAsync( + T source, + int first, + int last, + string after, + string before, + CancellationToken cancellationToken); } } From 9bc61b38409fbb490e9fbff3f8738d9d00ce8332 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 8 May 2020 14:25:07 +0200 Subject: [PATCH 02/15] wip --- .../Types/Types/Relay/ConnectionMiddleware.cs | 49 ++++++++++++ .../PagingObjectFieldDescriptorExtensions.cs | 4 +- .../Types/Types/Relay/IConnectionResolver.cs | 36 +++++---- .../Types/Relay/IConnectionResolver~1.cs | 33 +++++++++ src/Core/Types/Types/Relay/PageableData.cs | 38 ---------- .../Relay/QueryableConnectionMiddleware.cs | 74 ------------------- .../Relay/QueryableConnectionResolver.cs | 23 +++++- 7 files changed, 124 insertions(+), 133 deletions(-) create mode 100644 src/Core/Types/Types/Relay/ConnectionMiddleware.cs create mode 100644 src/Core/Types/Types/Relay/IConnectionResolver~1.cs delete mode 100644 src/Core/Types/Types/Relay/PageableData.cs delete mode 100644 src/Core/Types/Types/Relay/QueryableConnectionMiddleware.cs diff --git a/src/Core/Types/Types/Relay/ConnectionMiddleware.cs b/src/Core/Types/Types/Relay/ConnectionMiddleware.cs new file mode 100644 index 00000000000..743feeeda89 --- /dev/null +++ b/src/Core/Types/Types/Relay/ConnectionMiddleware.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using HotChocolate.Resolvers; + +namespace HotChocolate.Types.Relay +{ + public class ConnectionMiddleware + { + private readonly FieldDelegate _next; + + public ConnectionMiddleware(FieldDelegate next) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + } + + public async Task InvokeAsync( + IMiddlewareContext context, + IConnectionResolver connectionResolver) + { + await _next(context).ConfigureAwait(false); + + if (context.Result is IConnectionResolver localConnectionResolver) + { + context.Result = localConnectionResolver.ResolveAsync( + context, + default, + context.Argument("first"), + context.Argument("last"), + context.Argument("after"), + context.Argument("before"), + context.RequestAborted) + .ConfigureAwait(false); + } + + if (connectionResolver is { } && context.Result is T item) + { + context.Result = await connectionResolver.ResolveAsync( + context, + default, + context.Argument("first"), + context.Argument("last"), + context.Argument("after"), + context.Argument("before"), + context.RequestAborted) + .ConfigureAwait(false); + } + } + } +} diff --git a/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs b/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs index 67263ddb000..d4275eac645 100644 --- a/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs +++ b/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs @@ -14,7 +14,7 @@ public static IObjectFieldDescriptor UsePaging( return descriptor .AddPagingArguments() .Type>() - .Use>(); + .Use>(); } public static IObjectFieldDescriptor UsePaging( @@ -23,7 +23,7 @@ public static IObjectFieldDescriptor UsePaging( { FieldMiddleware placeholder = next => context => Task.CompletedTask; - Type middlewareDefinition = typeof(QueryableConnectionMiddleware<>); + Type middlewareDefinition = typeof(ConnectionMiddleware<>); descriptor .AddPagingArguments() diff --git a/src/Core/Types/Types/Relay/IConnectionResolver.cs b/src/Core/Types/Types/Relay/IConnectionResolver.cs index 49ae1894292..bb777809ae5 100644 --- a/src/Core/Types/Types/Relay/IConnectionResolver.cs +++ b/src/Core/Types/Types/Relay/IConnectionResolver.cs @@ -1,27 +1,31 @@ using System.Threading; using System.Threading.Tasks; +using HotChocolate.Resolvers; namespace HotChocolate.Types.Relay { public interface IConnectionResolver { + /// + /// Resolves a connection for a pageable data source. + /// + /// The middleware context. + /// The data source. + /// + /// + /// The cursor after which entities shall be taken. + /// The cursor before which entities shall be taken. + /// The cancellation token. + /// + /// Returns a connection which represents a page in the result set. + /// Task ResolveAsync( - object source, - int first, - int last, - string after, - string before, - CancellationToken cancellationToken); - } - - public interface IConnectionResolver : IConnectionResolver - { - Task ResolveAsync( - T source, - int first, - int last, - string after, - string before, + IMiddlewareContext context, + object source, + int? first, + int? last, + string? after, + string? before, CancellationToken cancellationToken); } } diff --git a/src/Core/Types/Types/Relay/IConnectionResolver~1.cs b/src/Core/Types/Types/Relay/IConnectionResolver~1.cs new file mode 100644 index 00000000000..847eb551bde --- /dev/null +++ b/src/Core/Types/Types/Relay/IConnectionResolver~1.cs @@ -0,0 +1,33 @@ +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Resolvers; + +#nullable enable + +namespace HotChocolate.Types.Relay +{ + public interface IConnectionResolver : IConnectionResolver + { + /// + /// Resolves a connection for a pageable data source. + /// + /// The middleware context. + /// The data source. + /// + /// + /// The cursor after which entities shall be taken. + /// The cursor before which entities shall be taken. + /// The cancellation token. + /// + /// Returns a connection which represents a page in the result set. + /// + Task ResolveAsync( + IMiddlewareContext context, + T source, + int? first, + int? last, + string? after, + string? before, + CancellationToken cancellationToken); + } +} diff --git a/src/Core/Types/Types/Relay/PageableData.cs b/src/Core/Types/Types/Relay/PageableData.cs deleted file mode 100644 index 0e870d8e014..00000000000 --- a/src/Core/Types/Types/Relay/PageableData.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace HotChocolate.Types.Relay -{ - public class PageableData - { - public PageableData(IEnumerable source) - : this(source, null) - { - } - - public PageableData( - IEnumerable source, - IDictionary properties) - : this(source?.AsQueryable(), properties) - { - } - - public PageableData(IQueryable source) - : this(source, null) - { - } - - public PageableData( - IQueryable source, - IDictionary properties) - { - Source = source ?? throw new ArgumentNullException(nameof(source)); - Properties = properties; - } - - public IQueryable Source { get; } - - public IDictionary Properties { get; } - } -} diff --git a/src/Core/Types/Types/Relay/QueryableConnectionMiddleware.cs b/src/Core/Types/Types/Relay/QueryableConnectionMiddleware.cs deleted file mode 100644 index ba3aac797bc..00000000000 --- a/src/Core/Types/Types/Relay/QueryableConnectionMiddleware.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using HotChocolate.Resolvers; - -namespace HotChocolate.Types.Relay -{ - public delegate IConnectionResolver ConnectionResolverFactory( - IQueryable source, - PagingDetails pagingDetails); - - public class QueryableConnectionMiddleware - { - private readonly FieldDelegate _next; - private readonly ConnectionResolverFactory _createConnectionResolver; - - public QueryableConnectionMiddleware( - FieldDelegate next, - ConnectionResolverFactory createConnectionResolver) - { - _next = next ?? throw new ArgumentNullException(nameof(next)); - _createConnectionResolver = createConnectionResolver - ?? CreateConnectionResolver; - } - - public async Task InvokeAsync(IMiddlewareContext context) - { - await _next(context).ConfigureAwait(false); - - var pagingDetails = new PagingDetails - { - First = context.Argument("first"), - After = context.Argument("after"), - Last = context.Argument("last"), - Before = context.Argument("before"), - }; - - IQueryable source = null; - - if (context.Result is PageableData p) - { - source = p.Source; - pagingDetails.Properties = p.Properties; - } - - if (context.Result is IQueryable q) - { - source = q; - } - else if (context.Result is IEnumerable e) - { - source = e.AsQueryable(); - } - - if (source != null) - { - IConnectionResolver connectionResolver = _createConnectionResolver( - source, pagingDetails); - - context.Result = await connectionResolver - .ResolveAsync(context.RequestAborted) - .ConfigureAwait(false); - } - } - - private static IConnectionResolver CreateConnectionResolver( - IQueryable source, PagingDetails pagingDetails) - { - return new QueryableConnectionResolver( - source, pagingDetails); - } - } -} diff --git a/src/Core/Types/Types/Relay/QueryableConnectionResolver.cs b/src/Core/Types/Types/Relay/QueryableConnectionResolver.cs index dd458f252dd..f4ba175a7ec 100644 --- a/src/Core/Types/Types/Relay/QueryableConnectionResolver.cs +++ b/src/Core/Types/Types/Relay/QueryableConnectionResolver.cs @@ -9,7 +9,7 @@ namespace HotChocolate.Types.Relay { public class QueryableConnectionResolver - : IConnectionResolver + : IConnectionResolver> { private const string _totalCount = "__totalCount"; private const string _position = "__position"; @@ -39,6 +39,22 @@ public QueryableConnectionResolver( ?? new Dictionary(); } + public Task ResolveAsync( + IQueryable source, + int first, + int last, + string after, + string before, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task ResolveAsync(object source, int first, int last, string after, string before, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + public Task> ResolveAsync( CancellationToken cancellationToken) { @@ -229,6 +245,8 @@ private static Dictionary TryDeserializeCursor( return properties; } + + protected class QueryablePagingDetails { public long? TotalCount { get; set; } @@ -238,8 +256,7 @@ protected class QueryablePagingDetails public int? Last { get; set; } } - protected class QueryableEdge - : Edge + protected class QueryableEdge : Edge { public QueryableEdge(string cursor, T node, int index) : base(cursor, node) From 7021c9434ef674328c708a56f513f93a59bbac89 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 15 May 2020 18:21:57 +0200 Subject: [PATCH 03/15] reworked queryable connection resolver --- .../Types/Types/Relay/ConnectionArguments.cs | 29 ++ src/Core/Types/Types/Relay/Edge.cs | 11 +- .../Types/Types/Relay/IConnectionResolver.cs | 34 ++- .../Types/Relay/IConnectionResolver~1.cs | 32 ++- src/Core/Types/Types/Relay/IndexEdge.cs | 77 ++++++ src/Core/Types/Types/Relay/PageInfo.cs | 9 +- src/Core/Types/Types/Relay/PagingDetails.cs | 13 - .../Relay/QueryableConnectionResolver.cs | 258 ++++++------------ 8 files changed, 246 insertions(+), 217 deletions(-) create mode 100644 src/Core/Types/Types/Relay/ConnectionArguments.cs create mode 100644 src/Core/Types/Types/Relay/IndexEdge.cs delete mode 100644 src/Core/Types/Types/Relay/PagingDetails.cs diff --git a/src/Core/Types/Types/Relay/ConnectionArguments.cs b/src/Core/Types/Types/Relay/ConnectionArguments.cs new file mode 100644 index 00000000000..c5f579d9d0a --- /dev/null +++ b/src/Core/Types/Types/Relay/ConnectionArguments.cs @@ -0,0 +1,29 @@ +#nullable enable + +namespace HotChocolate.Types.Relay +{ + public readonly struct ConnectionArguments + { + public ConnectionArguments(int? first, int? last, string? after, string? before) + { + First = first; + Last = last; + After = after; + Before = before; + } + + public int? First { get; } + + public int? Last { get; } + + /// + /// The cursor after which entities shall be taken. + /// + public string? After { get; } + + /// + /// The cursor before which entities shall be taken. + /// + public string? Before { get; } + } +} diff --git a/src/Core/Types/Types/Relay/Edge.cs b/src/Core/Types/Types/Relay/Edge.cs index 22765d002a6..dbdb53599b7 100644 --- a/src/Core/Types/Types/Relay/Edge.cs +++ b/src/Core/Types/Types/Relay/Edge.cs @@ -3,10 +3,9 @@ namespace HotChocolate.Types.Relay { - public class Edge - : IEdge + public class Edge : IEdge { - public Edge(string cursor, T node) + public Edge(T node, string cursor) { if (string.IsNullOrEmpty(cursor)) { @@ -15,14 +14,14 @@ public Edge(string cursor, T node) nameof(cursor)); } - Cursor = cursor; Node = node; + Cursor = cursor; } - public string Cursor { get; } - public T Node { get; } object IEdge.Node => Node; + + public string Cursor { get; } } } diff --git a/src/Core/Types/Types/Relay/IConnectionResolver.cs b/src/Core/Types/Types/Relay/IConnectionResolver.cs index bb777809ae5..cfdf95fdc49 100644 --- a/src/Core/Types/Types/Relay/IConnectionResolver.cs +++ b/src/Core/Types/Types/Relay/IConnectionResolver.cs @@ -2,6 +2,8 @@ using System.Threading.Tasks; using HotChocolate.Resolvers; +#nullable enable + namespace HotChocolate.Types.Relay { public interface IConnectionResolver @@ -9,23 +11,29 @@ public interface IConnectionResolver /// /// Resolves a connection for a pageable data source. /// - /// The middleware context. - /// The data source. - /// - /// - /// The cursor after which entities shall be taken. - /// The cursor before which entities shall be taken. - /// The cancellation token. + /// + /// The middleware context. + /// + /// + /// The data source. + /// + /// + /// The connection arguments passed in from the query. + /// + /// + /// The middleware requested a connection with a total count. + /// + /// + /// The cancellation token. + /// /// /// Returns a connection which represents a page in the result set. /// - Task ResolveAsync( + ValueTask ResolveAsync( IMiddlewareContext context, object source, - int? first, - int? last, - string? after, - string? before, - CancellationToken cancellationToken); + ConnectionArguments arguments = default, + bool withTotalCount = false, + CancellationToken cancellationToken = default); } } diff --git a/src/Core/Types/Types/Relay/IConnectionResolver~1.cs b/src/Core/Types/Types/Relay/IConnectionResolver~1.cs index 847eb551bde..efbfbea3049 100644 --- a/src/Core/Types/Types/Relay/IConnectionResolver~1.cs +++ b/src/Core/Types/Types/Relay/IConnectionResolver~1.cs @@ -11,23 +11,29 @@ public interface IConnectionResolver : IConnectionResolver /// /// Resolves a connection for a pageable data source. /// - /// The middleware context. - /// The data source. - /// - /// - /// The cursor after which entities shall be taken. - /// The cursor before which entities shall be taken. - /// The cancellation token. + /// + /// The middleware context. + /// + /// + /// The data source. + /// + /// + /// The connection arguments passed in from the query. + /// + /// + /// The middleware requested a connection with a total count. + /// + /// + /// The cancellation token. + /// /// /// Returns a connection which represents a page in the result set. /// - Task ResolveAsync( + ValueTask ResolveAsync( IMiddlewareContext context, T source, - int? first, - int? last, - string? after, - string? before, - CancellationToken cancellationToken); + ConnectionArguments arguments = default, + bool withTotalCount = false, + CancellationToken cancellationToken = default); } } diff --git a/src/Core/Types/Types/Relay/IndexEdge.cs b/src/Core/Types/Types/Relay/IndexEdge.cs new file mode 100644 index 00000000000..444b53fdd5a --- /dev/null +++ b/src/Core/Types/Types/Relay/IndexEdge.cs @@ -0,0 +1,77 @@ +using System; +using System.Buffers; +using System.Buffers.Text; +using System.Text; + +#nullable enable + +namespace HotChocolate.Types.Relay +{ + public sealed class IndexEdge : Edge + { + private static readonly Encoding _utf8 = Encoding.UTF8; + + private IndexEdge(T node, string cursor, int index) + : base(node, cursor) + { + Index = index; + } + + public int Index { get; set; } + + public static IndexEdge Create(T node, int index) + { + Span buffer = stackalloc byte[27 / 3 * 4]; + Utf8Formatter.TryFormat(index, buffer, out int written); + Base64.EncodeToUtf8InPlace(buffer, written, out written); + string cursor = CreateString(buffer.Slice(0, written)); + return new IndexEdge(node, cursor, index); + } + + private static unsafe string CreateString(Span buffer) + { + fixed (byte* bytePtr = buffer) + { + return _utf8.GetString(bytePtr, buffer.Length); + } + } + + public static unsafe int DeserializeCursor(string cursor) + { + fixed (char* cPtr = cursor) + { + int count = _utf8.GetByteCount(cPtr, cursor.Length); + byte[]? rented = null; + + Span buffer = count <= 128 + ? stackalloc byte[count] + : rented = ArrayPool.Shared.Rent(count); + + try + { + int written = 0; + fixed (byte* bytePtr = buffer) + { + written = _utf8.GetBytes(cPtr, cursor.Length, bytePtr, buffer.Length); + } + + Base64.EncodeToUtf8InPlace(buffer, written, out written); + if (Utf8Parser.TryParse(buffer.Slice(0, written), out int index, out _)) + { + return index; + } + + // todo : throwhelper => should be graphqlexeception => formatexception + throw new ArgumentOutOfRangeException("The cursor has an invalid format."); + } + finally + { + if (rented is { }) + { + ArrayPool.Shared.Return(rented); + } + } + } + } + } +} diff --git a/src/Core/Types/Types/Relay/PageInfo.cs b/src/Core/Types/Types/Relay/PageInfo.cs index 1dabb966e5b..4866ed64c28 100644 --- a/src/Core/Types/Types/Relay/PageInfo.cs +++ b/src/Core/Types/Types/Relay/PageInfo.cs @@ -1,11 +1,12 @@ namespace HotChocolate.Types.Relay { - public class PageInfo - : IPageInfo + public readonly struct PageInfo : IPageInfo { public PageInfo( - bool hasNextPage, bool hasPreviousPage, - string startCursor, string endCursor, + bool hasNextPage, + bool hasPreviousPage, + string startCursor, + string endCursor, long? totalCount) { HasNextPage = hasNextPage; diff --git a/src/Core/Types/Types/Relay/PagingDetails.cs b/src/Core/Types/Types/Relay/PagingDetails.cs deleted file mode 100644 index 6faf9616882..00000000000 --- a/src/Core/Types/Types/Relay/PagingDetails.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; - -namespace HotChocolate.Types.Relay -{ - public class PagingDetails - { - public IDictionary Properties { get; set; } - public string Before { get; set; } - public string After { get; set; } - public int? First { get; set; } - public int? Last { get; set; } - } -} diff --git a/src/Core/Types/Types/Relay/QueryableConnectionResolver.cs b/src/Core/Types/Types/Relay/QueryableConnectionResolver.cs index f4ba175a7ec..d7a1d36ea27 100644 --- a/src/Core/Types/Types/Relay/QueryableConnectionResolver.cs +++ b/src/Core/Types/Types/Relay/QueryableConnectionResolver.cs @@ -4,137 +4,133 @@ using System.Threading; using System.Threading.Tasks; using HotChocolate.Language; -using HotChocolate.Utilities; +using HotChocolate.Resolvers; namespace HotChocolate.Types.Relay { public class QueryableConnectionResolver : IConnectionResolver> { - private const string _totalCount = "__totalCount"; - private const string _position = "__position"; - - private readonly IQueryable _source; - private readonly IDictionary _properties; - private readonly QueryablePagingDetails _pageDetails; - - public QueryableConnectionResolver( + public async ValueTask ResolveAsync( + IMiddlewareContext context, IQueryable source, - PagingDetails pagingDetails) + ConnectionArguments arguments = default, + bool withTotalCount = false, + CancellationToken cancellationToken = default) { - if (source == null) - { - throw new ArgumentNullException(nameof(source)); - } + int? count = withTotalCount + ? (int?)await Task.Run(() => source.Count(), cancellationToken) + .ConfigureAwait(false) + : null; - if (pagingDetails == null) - { - throw new ArgumentNullException(nameof(pagingDetails)); - } + int? after = arguments.After is { } a + ? (int?)IndexEdge.DeserializeCursor(a) + : null; - _source = source; - _pageDetails = DeserializePagingDetails(pagingDetails); + int? before = arguments.Before is { } b + ? (int?)IndexEdge.DeserializeCursor(b) + : null; - _properties = pagingDetails.Properties - ?? new Dictionary(); - } + List> selectedEdges = + await GetSelectedEdgesAsync( + source, arguments.First, arguments.Last, after, before, cancellationToken) + .ConfigureAwait(false); - public Task ResolveAsync( - IQueryable source, - int first, - int last, - string after, - string before, - CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task ResolveAsync(object source, int first, int last, string after, string before, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task> ResolveAsync( - CancellationToken cancellationToken) - { - return Task.Run(() => Create(), cancellationToken); - } - - private Connection Create() - { - if (!_pageDetails.TotalCount.HasValue) - { - _pageDetails.TotalCount = _source.Count(); - } - - _properties[_totalCount] = _pageDetails.TotalCount.Value; - - IReadOnlyCollection selectedEdges = - GetSelectedEdges(); - QueryableEdge firstEdge = selectedEdges.FirstOrDefault(); - QueryableEdge lastEdge = selectedEdges.LastOrDefault(); + IndexEdge firstEdge = selectedEdges[0]; + IndexEdge lastEdge = selectedEdges[selectedEdges.Count - 1]; var pageInfo = new PageInfo( - lastEdge?.Index < (_pageDetails.TotalCount.Value - 1), + lastEdge?.Index < (count.Value - 1), firstEdge?.Index > 0, - selectedEdges.FirstOrDefault()?.Cursor, - selectedEdges.LastOrDefault()?.Cursor, - _pageDetails.TotalCount); + firstEdge?.Cursor, + lastEdge?.Cursor, + count); return new Connection(pageInfo, selectedEdges); } - Task IConnectionResolver.ResolveAsync( + ValueTask IConnectionResolver.ResolveAsync( + IMiddlewareContext context, + object source, + ConnectionArguments arguments, + bool withTotalCount, CancellationToken cancellationToken) => - Task.Run(() => Create(), cancellationToken); - - private IReadOnlyCollection GetSelectedEdges() + ResolveAsync( + context, + (IQueryable)source, + arguments, + withTotalCount, + cancellationToken); + + private async ValueTask>> GetSelectedEdgesAsync( + IQueryable allEdges, + int? first, + int? last, + int? after, + int? before, + CancellationToken cancellationToken) { - var list = new List(); - List edges = GetEdgesToReturn( - _source, _pageDetails, out int offset); + IQueryable edges = GetEdgesToReturn( + allEdges, first, last, after, before, + out int offset); + + var list = new List>(); - for (int i = 0; i < edges.Count; i++) + if (edges is IAsyncEnumerable enumerable) + { + int index = offset; + await foreach (T item in enumerable.WithCancellation(cancellationToken) + .ConfigureAwait(false)) + { + list.Add(IndexEdge.Create(item, index++)); + } + } + else { - int index = offset + i; - _properties[_position] = index; - string cursor = Base64Serializer.Serialize(_properties); - list.Add(new QueryableEdge(cursor, edges[i], index)); + await Task.Run(() => + { + int index = offset; + foreach (T item in edges) + { + if (cancellationToken.IsCancellationRequested) + { + break; + } + list.Add(IndexEdge.Create(item, index++)); + } + }).ConfigureAwait(false); } return list; } - private List GetEdgesToReturn( + private IQueryable GetEdgesToReturn( IQueryable allEdges, - QueryablePagingDetails pagingDetails, + int? first, + int? last, + int? after, + int? before, out int offset) { - IQueryable edges = ApplyCursorToEdges( - allEdges, pagingDetails.Before, pagingDetails.After); + IQueryable edges = ApplyCursorToEdges(allEdges, before, after); offset = 0; - if (pagingDetails.After.HasValue) + if (after.HasValue) { - offset = pagingDetails.After.Value + 1; + offset = after.Value + 1; } - if (pagingDetails.First.HasValue) + if (first.HasValue) { - edges = GetFirstEdges( - edges, pagingDetails.First.Value, - ref offset); + edges = GetFirstEdges(edges, first.Value, ref offset); } - if (pagingDetails.Last.HasValue) + if (last.HasValue) { - edges = GetLastEdges( - edges, pagingDetails.Last.Value, - ref offset); + edges = GetLastEdges(edges, last.Value, ref offset); } - return edges.ToList(); + return edges; } protected virtual IQueryable GetFirstEdges( @@ -149,7 +145,8 @@ protected virtual IQueryable GetFirstEdges( } protected virtual IQueryable GetLastEdges( - IQueryable edges, int last, + IQueryable edges, + int last, ref int offset) { if (last < 0) @@ -173,7 +170,9 @@ protected virtual IQueryable GetLastEdges( } protected virtual IQueryable ApplyCursorToEdges( - IQueryable allEdges, int? before, int? after) + IQueryable allEdges, + int? after, + int? before) { IQueryable edges = allEdges; @@ -189,82 +188,5 @@ protected virtual IQueryable ApplyCursorToEdges( return edges; } - - private static QueryablePagingDetails DeserializePagingDetails( - PagingDetails pagingDetails) - { - Dictionary afterProperties = - TryDeserializeCursor(pagingDetails.After); - Dictionary beforeProperties = - TryDeserializeCursor(pagingDetails.Before); - - return new QueryablePagingDetails - { - After = GetPositionFromCurser(afterProperties), - Before = GetPositionFromCurser(beforeProperties), - TotalCount = GetTotalCountFromCursor(afterProperties) - ?? GetTotalCountFromCursor(beforeProperties), - First = pagingDetails.First, - Last = pagingDetails.Last - }; - } - - private static int? GetPositionFromCurser( - IDictionary properties) - { - if (properties == null) - { - return null; - } - - return Convert.ToInt32(properties[_position]); - } - - private static int? GetTotalCountFromCursor( - IDictionary properties) - { - if (properties == null) - { - return null; - } - - return Convert.ToInt32(properties[_totalCount]); - } - - private static Dictionary TryDeserializeCursor( - string cursor) - { - if (cursor == null) - { - return null; - } - - Dictionary properties = Base64Serializer - .Deserialize>(cursor); - - return properties; - } - - - - protected class QueryablePagingDetails - { - public long? TotalCount { get; set; } - public int? Before { get; set; } - public int? After { get; set; } - public int? First { get; set; } - public int? Last { get; set; } - } - - protected class QueryableEdge : Edge - { - public QueryableEdge(string cursor, T node, int index) - : base(cursor, node) - { - Index = index; - } - - public int Index { get; set; } - } } } From 45c6625e1a6e76d4d248d20ae8b138e612a4bc55 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 15 May 2020 18:27:04 +0200 Subject: [PATCH 04/15] Fixed middleware --- .../Types/Types/Relay/ConnectionMiddleware.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Core/Types/Types/Relay/ConnectionMiddleware.cs b/src/Core/Types/Types/Relay/ConnectionMiddleware.cs index 743feeeda89..1d9cc671eda 100644 --- a/src/Core/Types/Types/Relay/ConnectionMiddleware.cs +++ b/src/Core/Types/Types/Relay/ConnectionMiddleware.cs @@ -19,15 +19,19 @@ public async Task InvokeAsync( { await _next(context).ConfigureAwait(false); + var arguments = new ConnectionArguments( + context.Argument("first"), + context.Argument("last"), + context.Argument("after"), + context.Argument("before")); + if (context.Result is IConnectionResolver localConnectionResolver) { context.Result = localConnectionResolver.ResolveAsync( context, default, - context.Argument("first"), - context.Argument("last"), - context.Argument("after"), - context.Argument("before"), + arguments, + true, // where should we store this? context.RequestAborted) .ConfigureAwait(false); } @@ -37,10 +41,8 @@ public async Task InvokeAsync( context.Result = await connectionResolver.ResolveAsync( context, default, - context.Argument("first"), - context.Argument("last"), - context.Argument("after"), - context.Argument("before"), + arguments, + true, // where should we store this? context.RequestAborted) .ConfigureAwait(false); } From d923fbdd11881db5883b01e702bad9a49ac5820f Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 15 May 2020 18:31:24 +0200 Subject: [PATCH 05/15] fixed more compile tests --- .../QueryableFilterMiddleware.cs | 13 +-- .../Types.Sorting/QueryableSortMiddleware.cs | 13 +-- .../Types/Relay/PageableDataTests.cs | 80 ------------------- 3 files changed, 2 insertions(+), 104 deletions(-) delete mode 100644 src/Core/Types.Tests/Types/Relay/PageableDataTests.cs diff --git a/src/Core/Types.Filters/QueryableFilterMiddleware.cs b/src/Core/Types.Filters/QueryableFilterMiddleware.cs index 4deffc33153..6a7b1008730 100644 --- a/src/Core/Types.Filters/QueryableFilterMiddleware.cs +++ b/src/Core/Types.Filters/QueryableFilterMiddleware.cs @@ -35,15 +35,6 @@ public async Task InvokeAsync(IMiddlewareContext context) IQueryable source = null; - if (context.Result is PageableData p) - { - source = p.Source; - } - else - { - p = null; - } - if (context.Result is IQueryable q) { source = q; @@ -64,9 +55,7 @@ public async Task InvokeAsync(IMiddlewareContext context) filter.Accept(visitor); source = source.Where(visitor.CreateFilter()); - context.Result = p is null - ? (object)source - : new PageableData(source, p.Properties); + context.Result = source; } } } diff --git a/src/Core/Types.Sorting/QueryableSortMiddleware.cs b/src/Core/Types.Sorting/QueryableSortMiddleware.cs index 20508bfd8cc..d7c8b8a4eb7 100644 --- a/src/Core/Types.Sorting/QueryableSortMiddleware.cs +++ b/src/Core/Types.Sorting/QueryableSortMiddleware.cs @@ -41,15 +41,6 @@ public async Task InvokeAsync(IMiddlewareContext context) source = e.AsQueryable(); } - if (context.Result is PageableData p) - { - source = p.Source; - } - else - { - p = null; - } - if (source != null && context.Field .Arguments[SortObjectFieldDescriptorExtensions.OrderByArgumentName] @@ -62,9 +53,7 @@ .Type is InputObjectType iot sortArgument.Accept(visitor); source = visitor.Sort(source); - context.Result = p is null - ? (object)source - : new PageableData(source, p.Properties); + context.Result = source; } } } diff --git a/src/Core/Types.Tests/Types/Relay/PageableDataTests.cs b/src/Core/Types.Tests/Types/Relay/PageableDataTests.cs deleted file mode 100644 index fb972cc316b..00000000000 --- a/src/Core/Types.Tests/Types/Relay/PageableDataTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Xunit; - -namespace HotChocolate.Types.Relay -{ - public class PageableDataTests - { - [Fact] - public void Create_WithEnumerable_ArgumentIsCorrectlyPassed() - { - // arrange - // act - var data = new PageableData(new[] { "a", "b" }); - - // assert - Assert.Collection(data.Source, - t => Assert.Equal("a", t), - t => Assert.Equal("b", t)); - Assert.Null(data.Properties); - } - - [Fact] - public void Create_WithEnumerableAndProps_ArgumentIsCorrectlyPassed() - { - // arrange - // act - var data = new PageableData( - new[] { "a", "b" }, - new Dictionary { { "a", "b" } }); - - // assert - Assert.Collection(data.Source, - t => Assert.Equal("a", t), - t => Assert.Equal("b", t)); - Assert.Collection(data.Properties, - t => - { - Assert.Equal("a", t.Key); - Assert.Equal("b", t.Value); - }); - } - - [Fact] - public void Create_WithQueryable_ArgumentIsCorrectlyPassed() - { - // arrange - // act - var data = new PageableData( - new[] { "a", "b" }.AsQueryable()); - - // assert - Assert.Collection(data.Source, - t => Assert.Equal("a", t), - t => Assert.Equal("b", t)); - Assert.Null(data.Properties); - } - - [Fact] - public void Create_WithQueryableAndProps_ArgumentIsCorrectlyPassed() - { - // arrange - // act - var data = new PageableData( - new[] { "a", "b" }.AsQueryable(), - new Dictionary { { "a", "b" } }); - - // assert - Assert.Collection(data.Source, - t => Assert.Equal("a", t), - t => Assert.Equal("b", t)); - Assert.Collection(data.Properties, - t => - { - Assert.Equal("a", t.Key); - Assert.Equal("b", t.Value); - }); - } - } -} From 8d9eaf297b5ea59f9f91f6542ce7a4f6aa5ce892 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 15 May 2020 22:55:17 +0200 Subject: [PATCH 06/15] Fixed Connection Tests --- .../Types/Relay/QueryableConnectionTests.cs | 229 ++++++++---------- .../Types/Types/Relay/ConnectionArguments.cs | 6 +- .../PagingObjectFieldDescriptorExtensions.cs | 31 ++- src/Core/Types/Types/Relay/IndexEdge.cs | 2 +- .../Relay/QueryableConnectionResolver.cs | 2 +- .../Types/Types/Relay/UsePagingAttribute.cs | 12 +- 6 files changed, 136 insertions(+), 146 deletions(-) diff --git a/src/Core/Types.Tests/Types/Relay/QueryableConnectionTests.cs b/src/Core/Types.Tests/Types/Relay/QueryableConnectionTests.cs index 453535a8eff..bf6c8cc900e 100644 --- a/src/Core/Types.Tests/Types/Relay/QueryableConnectionTests.cs +++ b/src/Core/Types.Tests/Types/Relay/QueryableConnectionTests.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; +using System.Text; using System.Threading.Tasks; -using HotChocolate.Utilities; +using HotChocolate.Resolvers; +using Moq; using Xunit; namespace HotChocolate.Types.Relay @@ -14,19 +15,16 @@ public class QueryableConnectionTests public async Task TakeFirst() { // arrange + var context = new Mock(); var list = new List { "a", "b", "c", "d", "e", "f", "g", }; - - var pagingDetails = new PagingDetails - { - First = 2 - }; - - var connectionFactory = new QueryableConnectionResolver( - list.AsQueryable(), pagingDetails); + var connectionFactory = new QueryableConnectionResolver(); // act - Connection connection = await connectionFactory.ResolveAsync( - CancellationToken.None); + IConnection connection = await connectionFactory.ResolveAsync( + context.Object, + list.AsQueryable(), + new ConnectionArguments(2), + true); // assert Assert.Collection(connection.Edges, @@ -54,19 +52,16 @@ public async Task TakeFirst() public async Task TakeLast() { // arrange + var context = new Mock(); var list = new List { "a", "b", "c", "d", "e", "f", "g", }; - - var pagingDetails = new PagingDetails - { - Last = 2 - }; - - var connectionFactory = new QueryableConnectionResolver( - list.AsQueryable(), pagingDetails); + var connectionFactory = new QueryableConnectionResolver(); // act - Connection connection = await connectionFactory.ResolveAsync( - CancellationToken.None); + IConnection connection = await connectionFactory.ResolveAsync( + context.Object, + list.AsQueryable(), + new ConnectionArguments(last: 2), + true); // assert Assert.Collection(connection.Edges, @@ -94,26 +89,22 @@ public async Task TakeLast() public async Task TakeFirstAfter() { // arrange + var context = new Mock(); var list = new List { "a", "b", "c", "d", "e", "f", "g", }; + var connectionFactory = new QueryableConnectionResolver(); - var connectionFactory = new QueryableConnectionResolver( - list.AsQueryable(), new PagingDetails { First = 1 }); - - Connection connection = await connectionFactory.ResolveAsync( - CancellationToken.None); - - var pagingDetails = new PagingDetails - { - After = connection.PageInfo.StartCursor, - First = 2 - }; - - connectionFactory = new QueryableConnectionResolver( - list.AsQueryable(), pagingDetails); + IConnection connection = await connectionFactory.ResolveAsync( + context.Object, + list.AsQueryable(), + new ConnectionArguments(1), + true); // act connection = await connectionFactory.ResolveAsync( - CancellationToken.None); + context.Object, + list.AsQueryable(), + new ConnectionArguments(2, after: connection.PageInfo.StartCursor), + true); // assert Assert.Collection(connection.Edges, @@ -141,41 +132,28 @@ public async Task TakeFirstAfter() public async Task TakeTwoAfterSecondTime() { // arrange + var context = new Mock(); var list = new List { "a", "b", "c", "d", "e", "f", "g", }; + var connectionFactory = new QueryableConnectionResolver(); - // 1. Page - var connectionFactory = new QueryableConnectionResolver( - list.AsQueryable(), new PagingDetails { First = 2 }); - - Connection connection = await connectionFactory.ResolveAsync( - CancellationToken.None); - - //2. Page - var pagingDetails = new PagingDetails - { - After = connection.PageInfo.EndCursor, - First = 2 - }; - - connectionFactory = new QueryableConnectionResolver( - list.AsQueryable(), pagingDetails); + IConnection connection = await connectionFactory.ResolveAsync( + context.Object, + list.AsQueryable(), + new ConnectionArguments(2), + true); connection = await connectionFactory.ResolveAsync( - CancellationToken.None); - - //3. Page - pagingDetails = new PagingDetails - { - After = connection.PageInfo.EndCursor, - First = 2 - }; - - connectionFactory = new QueryableConnectionResolver( - list.AsQueryable(), pagingDetails); + context.Object, + list.AsQueryable(), + new ConnectionArguments(2, after: connection.PageInfo.EndCursor), + true); // act connection = await connectionFactory.ResolveAsync( - CancellationToken.None); + context.Object, + list.AsQueryable(), + new ConnectionArguments(2, after: connection.PageInfo.EndCursor), + true); // assert Assert.Collection(connection.Edges, @@ -203,26 +181,22 @@ public async Task TakeTwoAfterSecondTime() public async Task TakeLastBefore() { // arrange + var context = new Mock(); var list = new List { "a", "b", "c", "d", "e", "f", "g", }; + var connectionFactory = new QueryableConnectionResolver(); - var connectionFactory = new QueryableConnectionResolver( - list.AsQueryable(), new PagingDetails { First = 5 }); - - Connection connection = await connectionFactory.ResolveAsync( - CancellationToken.None); - - var pagingDetails = new PagingDetails - { - Before = connection.PageInfo.EndCursor, - Last = 2 - }; - - connectionFactory = new QueryableConnectionResolver( - list.AsQueryable(), pagingDetails); + IConnection connection = await connectionFactory.ResolveAsync( + context.Object, + list.AsQueryable(), + new ConnectionArguments(5), + true); // act connection = await connectionFactory.ResolveAsync( - CancellationToken.None); + context.Object, + list.AsQueryable(), + new ConnectionArguments(last: 2, before: connection.PageInfo.EndCursor), + true); // assert Assert.Collection(connection.Edges, @@ -250,19 +224,16 @@ public async Task TakeLastBefore() public async Task HasNextPage_True() { // arrange + var context = new Mock(); var list = new List { "a", "b", "c", "d", "e", "f", "g", }; - - var pagingDetails = new PagingDetails - { - First = 5 - }; - - var connectionFactory = new QueryableConnectionResolver( - list.AsQueryable(), pagingDetails); + var connectionFactory = new QueryableConnectionResolver(); // act - Connection connection = await connectionFactory.ResolveAsync( - CancellationToken.None); + IConnection connection = await connectionFactory.ResolveAsync( + context.Object, + list.AsQueryable(), + new ConnectionArguments(5), + true); // assert Assert.True(connection.PageInfo.HasNextPage); @@ -272,19 +243,16 @@ public async Task HasNextPage_True() public async Task HasNextPage_False() { // arrange + var context = new Mock(); var list = new List { "a", "b", "c", "d", "e", "f", "g", }; - - var pagingDetails = new PagingDetails - { - First = 7 - }; - - var connectionFactory = new QueryableConnectionResolver( - list.AsQueryable(), pagingDetails); + var connectionFactory = new QueryableConnectionResolver(); // act - Connection connection = await connectionFactory.ResolveAsync( - CancellationToken.None); + IConnection connection = await connectionFactory.ResolveAsync( + context.Object, + list.AsQueryable(), + new ConnectionArguments(7), + true); // assert Assert.False(connection.PageInfo.HasNextPage); @@ -294,26 +262,22 @@ public async Task HasNextPage_False() public async Task HasPrevious_True() { // arrange + var context = new Mock(); var list = new List { "a", "b", "c", "d", "e", "f", "g", }; + var connectionFactory = new QueryableConnectionResolver(); - var connectionFactory = new QueryableConnectionResolver( - list.AsQueryable(), new PagingDetails { First = 1 }); - - Connection connection = await connectionFactory.ResolveAsync( - CancellationToken.None); - - var pagingDetails = new PagingDetails - { - After = connection.PageInfo.StartCursor, - First = 2 - }; - - connectionFactory = new QueryableConnectionResolver( - list.AsQueryable(), pagingDetails); + IConnection connection = await connectionFactory.ResolveAsync( + context.Object, + list.AsQueryable(), + new ConnectionArguments(1), + true); // act connection = await connectionFactory.ResolveAsync( - CancellationToken.None); + context.Object, + list.AsQueryable(), + new ConnectionArguments(first: 2, after: connection.PageInfo.StartCursor), + true); // assert Assert.True(connection.PageInfo.HasPreviousPage); @@ -323,26 +287,43 @@ public async Task HasPrevious_True() public async Task HasPrevious_False() { // arrange + var context = new Mock(); var list = new List { "a", "b", "c", "d", "e", "f", "g", }; + var connectionFactory = new QueryableConnectionResolver(); + + // act + IConnection connection = await connectionFactory.ResolveAsync( + context.Object, + list.AsQueryable(), + new ConnectionArguments(1), + true); - var pagingDetails = new PagingDetails(); + // assert + Assert.False(connection.PageInfo.HasPreviousPage); + } - var connectionFactory = new QueryableConnectionResolver( - list.AsQueryable(), pagingDetails); + [Fact] + public async Task TotalCount() + { + // arrange + var context = new Mock(); + var list = new List { "a", "b", "c", "d", "e", "f", "g", }; + var connectionFactory = new QueryableConnectionResolver(); // act - Connection connection = await connectionFactory.ResolveAsync( - CancellationToken.None); + IConnection connection = await connectionFactory.ResolveAsync( + context.Object, + list.AsQueryable(), + new ConnectionArguments(1), + true); // assert - Assert.False(connection.PageInfo.HasPreviousPage); + Assert.Equal(7, connection.PageInfo.TotalCount); } private int GetPositionFromCursor(string cursor) { - Dictionary properties = Base64Serializer - .Deserialize>(cursor); - return Convert.ToInt32(properties["__position"]); + return int.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(cursor))); } } } diff --git a/src/Core/Types/Types/Relay/ConnectionArguments.cs b/src/Core/Types/Types/Relay/ConnectionArguments.cs index c5f579d9d0a..cce973edd31 100644 --- a/src/Core/Types/Types/Relay/ConnectionArguments.cs +++ b/src/Core/Types/Types/Relay/ConnectionArguments.cs @@ -4,7 +4,11 @@ namespace HotChocolate.Types.Relay { public readonly struct ConnectionArguments { - public ConnectionArguments(int? first, int? last, string? after, string? before) + public ConnectionArguments( + int? first = null, + int? last = null, + string? after = null, + string? before = null) { First = first; Last = last; diff --git a/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs b/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs index d4275eac645..0d9c2d64164 100644 --- a/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs +++ b/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs @@ -7,6 +7,8 @@ namespace HotChocolate.Types.Relay { public static class PagingObjectFieldDescriptorExtensions { + private static readonly Type _middleware = typeof(ConnectionMiddleware<>); + public static IObjectFieldDescriptor UsePaging( this IObjectFieldDescriptor descriptor) where TSchemaType : class, IOutputType @@ -21,9 +23,7 @@ public static IObjectFieldDescriptor UsePaging( this IObjectFieldDescriptor descriptor) where TSchemaType : class, IOutputType { - FieldMiddleware placeholder = - next => context => Task.CompletedTask; - Type middlewareDefinition = typeof(ConnectionMiddleware<>); + FieldMiddleware placeholder = next => context => Task.CompletedTask; descriptor .AddPagingArguments() @@ -32,18 +32,13 @@ public static IObjectFieldDescriptor UsePaging( .Extend() .OnBeforeCompletion((context, defintion) => { - var reference = new ClrTypeReference( - typeof(TSchemaType), - TypeContext.Output); + var reference = new ClrTypeReference(typeof(TSchemaType), TypeContext.Output); IOutputType type = context.GetType(reference); + if (type.NamedType() is IHasClrType hasClrType) { - Type middlewareType = middlewareDefinition - .MakeGenericType(hasClrType.ClrType); - FieldMiddleware middleware = - FieldClassMiddlewareFactory.Create(middlewareType); - int index = - defintion.MiddlewareComponents.IndexOf(placeholder); + FieldMiddleware middleware = CreateMiddleware(hasClrType.ClrType); + int index = defintion.MiddlewareComponents.IndexOf(placeholder); defintion.MiddlewareComponents[index] = middleware; } }) @@ -82,5 +77,17 @@ public static IInterfaceFieldDescriptor AddPagingArguments( .Argument("last", a => a.Type()) .Argument("before", a => a.Type()); } + + private static FieldMiddleware CreateMiddleware(Type type) + { + if (type.IsGenericType && + typeof(IConnectionResolver<>) == type.GetGenericTypeDefinition()) + { + type = type.GetGenericArguments()[0]; + } + + Type middlewareType = _middleware.MakeGenericType(type); + return FieldClassMiddlewareFactory.Create(middlewareType); + } } } diff --git a/src/Core/Types/Types/Relay/IndexEdge.cs b/src/Core/Types/Types/Relay/IndexEdge.cs index 444b53fdd5a..dcf04501120 100644 --- a/src/Core/Types/Types/Relay/IndexEdge.cs +++ b/src/Core/Types/Types/Relay/IndexEdge.cs @@ -55,7 +55,7 @@ public static unsafe int DeserializeCursor(string cursor) written = _utf8.GetBytes(cPtr, cursor.Length, bytePtr, buffer.Length); } - Base64.EncodeToUtf8InPlace(buffer, written, out written); + Base64.DecodeFromUtf8InPlace(buffer, out written); if (Utf8Parser.TryParse(buffer.Slice(0, written), out int index, out _)) { return index; diff --git a/src/Core/Types/Types/Relay/QueryableConnectionResolver.cs b/src/Core/Types/Types/Relay/QueryableConnectionResolver.cs index d7a1d36ea27..7b6af6fed5f 100644 --- a/src/Core/Types/Types/Relay/QueryableConnectionResolver.cs +++ b/src/Core/Types/Types/Relay/QueryableConnectionResolver.cs @@ -112,7 +112,7 @@ private IQueryable GetEdgesToReturn( int? before, out int offset) { - IQueryable edges = ApplyCursorToEdges(allEdges, before, after); + IQueryable edges = ApplyCursorToEdges(allEdges, after, before); offset = 0; if (after.HasValue) diff --git a/src/Core/Types/Types/Relay/UsePagingAttribute.cs b/src/Core/Types/Types/Relay/UsePagingAttribute.cs index 44214d657ec..2c59345510a 100644 --- a/src/Core/Types/Types/Relay/UsePagingAttribute.cs +++ b/src/Core/Types/Types/Relay/UsePagingAttribute.cs @@ -55,12 +55,11 @@ private Type GetSchemaType( MemberInfo member) { Type? type = SchemaType; - ITypeReference returnType = context.Inspector.GetReturnType( - member, TypeContext.Output); + ITypeReference returnType = context.Inspector.GetReturnType(member, TypeContext.Output); - if (type is null - && returnType is IClrTypeReference clr - && TypeInspector.Default.TryCreate(clr.Type, out var typeInfo)) + if (type is null && + returnType is IClrTypeReference clr && + TypeInspector.Default.TryCreate(clr.Type, out Utilities.TypeInfo typeInfo)) { if (BaseTypes.IsSchemaType(typeInfo.ClrType)) { @@ -76,6 +75,7 @@ private Type GetSchemaType( if (type is null || !typeof(IType).IsAssignableFrom(type)) { + // TODO : ThrowHelper throw new SchemaException( SchemaErrorBuilder.New() .SetMessage("The UsePaging attribute needs a valid node schema type.") @@ -85,7 +85,5 @@ private Type GetSchemaType( return type; } - - } } From 7ba422cf0e285e2d7c8e226b08f9f900eb91d216 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 15 May 2020 23:26:58 +0200 Subject: [PATCH 07/15] wip --- src/Core/Core/Execution/Utilities/ResolverContext.cs | 3 +-- src/Core/Types/Types/Relay/ConnectionMiddleware.cs | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Core/Core/Execution/Utilities/ResolverContext.cs b/src/Core/Core/Execution/Utilities/ResolverContext.cs index 2c0b382800a..dc4b2bfaceb 100644 --- a/src/Core/Core/Execution/Utilities/ResolverContext.cs +++ b/src/Core/Core/Execution/Utilities/ResolverContext.cs @@ -120,8 +120,7 @@ public T Parent() return parent; } - if (_executionContext.Converter - .TryConvert(SourceObject, out parent)) + if (_executionContext.Converter.TryConvert(SourceObject, out parent)) { return parent; } diff --git a/src/Core/Types/Types/Relay/ConnectionMiddleware.cs b/src/Core/Types/Types/Relay/ConnectionMiddleware.cs index 1d9cc671eda..ed89809748d 100644 --- a/src/Core/Types/Types/Relay/ConnectionMiddleware.cs +++ b/src/Core/Types/Types/Relay/ConnectionMiddleware.cs @@ -4,7 +4,7 @@ namespace HotChocolate.Types.Relay { - public class ConnectionMiddleware + public class ConnectionMiddleware { private readonly FieldDelegate _next; @@ -15,7 +15,7 @@ public ConnectionMiddleware(FieldDelegate next) public async Task InvokeAsync( IMiddlewareContext context, - IConnectionResolver connectionResolver) + IConnectionResolver connectionResolver) { await _next(context).ConfigureAwait(false); @@ -36,7 +36,7 @@ public async Task InvokeAsync( .ConfigureAwait(false); } - if (connectionResolver is { } && context.Result is T item) + if (connectionResolver is { } && context.Result is TSource item) { context.Result = await connectionResolver.ResolveAsync( context, From 10b0f6a0a001e533abef87ad4355e646f3644b77 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sat, 16 May 2020 01:05:27 +0200 Subject: [PATCH 08/15] Reworked paging middleware --- src/Core/Types.Tests/Types/Relay/EdgeTests.cs | 6 +- ...ctionTypeTests.ExecuteQueryWithPaging.snap | 4 +- ...onTypeTests.UsePaging_WithComplexType.snap | 14 +-- ...sts.UsePaging_WithNonNull_ElementType.snap | 4 +- .../Types/Types/Relay/ConnectionMiddleware.cs | 34 ++++++- .../PagingObjectFieldDescriptorExtensions.cs | 91 ++++++++++++++----- .../Types/Utilities/DotNetTypeInfoFactory.cs | 21 ++++- 7 files changed, 129 insertions(+), 45 deletions(-) diff --git a/src/Core/Types.Tests/Types/Relay/EdgeTests.cs b/src/Core/Types.Tests/Types/Relay/EdgeTests.cs index 397a29eba9a..5c41b5c7ed7 100644 --- a/src/Core/Types.Tests/Types/Relay/EdgeTests.cs +++ b/src/Core/Types.Tests/Types/Relay/EdgeTests.cs @@ -13,7 +13,7 @@ public void CreateEdge_ArgumentsArePassedCorrectly( { // arrange // act - var edge = new Edge(cursor, node); + var edge = new Edge(node, cursor); // assert Assert.Equal(cursor, edge.Cursor); @@ -25,7 +25,7 @@ public void CreateEdge_CursorIsNull_ArgumentNullException() { // arrange // act - Action a = () => new Edge(null, "abc"); + Action a = () => new Edge("abc", null); // assert Assert.Throws(a); @@ -36,7 +36,7 @@ public void CreateEdge_CursorIsEmpty_ArgumentNullException() { // arrange // act - Action a = () => new Edge(string.Empty, "abc"); + Action a = () => new Edge("abc", string.Empty); // assert Assert.Throws(a); diff --git a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.ExecuteQueryWithPaging.snap b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.ExecuteQueryWithPaging.snap index 2e5e2c9ae78..7539e366bc7 100644 --- a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.ExecuteQueryWithPaging.snap +++ b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.ExecuteQueryWithPaging.snap @@ -3,11 +3,11 @@ "s": { "edges": [ { - "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjV9", + "cursor": "NQ==", "node": "f" }, { - "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjZ9", + "cursor": "Ng==", "node": "g" } ], diff --git a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePaging_WithComplexType.snap b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePaging_WithComplexType.snap index a090851e24b..b53d67a7ea7 100644 --- a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePaging_WithComplexType.snap +++ b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePaging_WithComplexType.snap @@ -4,31 +4,31 @@ "bar": { "edges": [ { - "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjB9", + "cursor": "MA==", "node": "a" }, { - "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjF9", + "cursor": "MQ==", "node": "b" }, { - "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjJ9", + "cursor": "Mg==", "node": "c" }, { - "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjN9", + "cursor": "Mw==", "node": "d" }, { - "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjR9", + "cursor": "NA==", "node": "e" }, { - "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjV9", + "cursor": "NQ==", "node": "f" }, { - "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjZ9", + "cursor": "Ng==", "node": "g" } ], diff --git a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePaging_WithNonNull_ElementType.snap b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePaging_WithNonNull_ElementType.snap index a018131d5a1..6edf6a5a6bc 100644 --- a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePaging_WithNonNull_ElementType.snap +++ b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePaging_WithNonNull_ElementType.snap @@ -3,11 +3,11 @@ "s": { "edges": [ { - "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjV9", + "cursor": "NQ==", "node": "f" }, { - "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjZ9", + "cursor": "Ng==", "node": "g" } ], diff --git a/src/Core/Types/Types/Relay/ConnectionMiddleware.cs b/src/Core/Types/Types/Relay/ConnectionMiddleware.cs index ed89809748d..dd5026129f6 100644 --- a/src/Core/Types/Types/Relay/ConnectionMiddleware.cs +++ b/src/Core/Types/Types/Relay/ConnectionMiddleware.cs @@ -1,11 +1,16 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using HotChocolate.Resolvers; namespace HotChocolate.Types.Relay { - public class ConnectionMiddleware + public class ConnectionMiddleware { + private readonly QueryableConnectionResolver _connectionResolver = + new QueryableConnectionResolver(); private readonly FieldDelegate _next; public ConnectionMiddleware(FieldDelegate next) @@ -29,18 +34,37 @@ public async Task InvokeAsync( { context.Result = localConnectionResolver.ResolveAsync( context, - default, + context.Result, arguments, true, // where should we store this? context.RequestAborted) .ConfigureAwait(false); } - - if (connectionResolver is { } && context.Result is TSource item) + else if (connectionResolver is { } && context.Result is TSource source) { context.Result = await connectionResolver.ResolveAsync( context, - default, + source, + arguments, + true, // where should we store this? + context.RequestAborted) + .ConfigureAwait(false); + } + else if (context.Result is IQueryable queryable) + { + context.Result = await _connectionResolver.ResolveAsync( + context, + queryable, + arguments, + true, // where should we store this? + context.RequestAborted) + .ConfigureAwait(false); + } + else if (context.Result is IEnumerable enumerable) + { + context.Result = await _connectionResolver.ResolveAsync( + context, + enumerable.AsQueryable(), arguments, true, // where should we store this? context.RequestAborted) diff --git a/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs b/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs index 0d9c2d64164..5e851f89790 100644 --- a/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs +++ b/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using System.Threading.Tasks; using HotChocolate.Resolvers; using HotChocolate.Types.Descriptors; @@ -7,20 +8,21 @@ namespace HotChocolate.Types.Relay { public static class PagingObjectFieldDescriptorExtensions { - private static readonly Type _middleware = typeof(ConnectionMiddleware<>); + private static readonly Type _middleware = typeof(ConnectionMiddleware<,>); - public static IObjectFieldDescriptor UsePaging( + public static IObjectFieldDescriptor UsePaging( this IObjectFieldDescriptor descriptor) - where TSchemaType : class, IOutputType - { - return descriptor - .AddPagingArguments() - .Type>() - .Use>(); - } + where TSchemaType : class, IOutputType => + UsePaging(descriptor, typeof(TEntity)); public static IObjectFieldDescriptor UsePaging( this IObjectFieldDescriptor descriptor) + where TSchemaType : class, IOutputType => + UsePaging(descriptor, null); + + private static IObjectFieldDescriptor UsePaging( + IObjectFieldDescriptor descriptor, + Type entityType) where TSchemaType : class, IOutputType { FieldMiddleware placeholder = next => context => Task.CompletedTask; @@ -32,15 +34,24 @@ public static IObjectFieldDescriptor UsePaging( .Extend() .OnBeforeCompletion((context, defintion) => { - var reference = new ClrTypeReference(typeof(TSchemaType), TypeContext.Output); - IOutputType type = context.GetType(reference); - - if (type.NamedType() is IHasClrType hasClrType) + if (entityType is null) { - FieldMiddleware middleware = CreateMiddleware(hasClrType.ClrType); - int index = defintion.MiddlewareComponents.IndexOf(placeholder); - defintion.MiddlewareComponents[index] = middleware; + var reference = new ClrTypeReference( + typeof(TSchemaType), + TypeContext.Output); + IOutputType type = context.GetType(reference); + entityType = ((IHasClrType)type.NamedType()).ClrType; } + + MemberInfo member = defintion.ResolverMember ?? defintion.Member; + Type resultType = defintion.Resolver is { } && defintion.ResultType is { } + ? defintion.ResultType + : GetResultType(member); + resultType = UnwrapType(resultType); + + FieldMiddleware middleware = CreateMiddleware(resultType, entityType); + int index = defintion.MiddlewareComponents.IndexOf(placeholder); + defintion.MiddlewareComponents[index] = middleware; }) .DependsOn(); @@ -78,16 +89,52 @@ public static IInterfaceFieldDescriptor AddPagingArguments( .Argument("before", a => a.Type()); } - private static FieldMiddleware CreateMiddleware(Type type) + private static FieldMiddleware CreateMiddleware(Type sourceType, Type entityType) + { + Type middlewareType = _middleware.MakeGenericType(sourceType, entityType); + return FieldClassMiddlewareFactory.Create(middlewareType); + } + + private static Type GetResultType(MemberInfo member) { - if (type.IsGenericType && - typeof(IConnectionResolver<>) == type.GetGenericTypeDefinition()) + if (member is PropertyInfo p) { - type = type.GetGenericArguments()[0]; + return p.PropertyType; } - Type middlewareType = _middleware.MakeGenericType(type); - return FieldClassMiddlewareFactory.Create(middlewareType); + if (member is MethodInfo m) + { + return m.ReturnType; + } + + throw new NotSupportedException(); + } + + internal static Type UnwrapType(Type resultType) + { + if (resultType.IsGenericType && + resultType.GetGenericTypeDefinition() == typeof(IConnectionResolver<>)) + { + return resultType.GetGenericArguments()[0]; + } + + if (typeof(IConnectionResolver).IsAssignableFrom(resultType)) + { + Type[] interfaces = resultType.GetInterfaces(); + for (int i = 0; i < interfaces.Length; i++) + { + Type type = interfaces[i]; + if (type.IsGenericType && + type.GetGenericTypeDefinition() == typeof(IConnectionResolver<>)) + { + return type.GetGenericArguments()[0]; + } + } + + return typeof(object); + } + + return resultType; } } } diff --git a/src/Core/Types/Utilities/DotNetTypeInfoFactory.cs b/src/Core/Types/Utilities/DotNetTypeInfoFactory.cs index 2e864b8f017..d553db86ef5 100644 --- a/src/Core/Types/Utilities/DotNetTypeInfoFactory.cs +++ b/src/Core/Types/Utilities/DotNetTypeInfoFactory.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using HotChocolate.Types; +using HotChocolate.Types.Relay; namespace HotChocolate.Utilities { @@ -41,7 +42,7 @@ public static Type Unwrap(Type type) public static Type UnwrapNonNull(Type type) { - if(IsNonNullType(type)) + if (IsNonNullType(type)) { return GetInnerType(type); } @@ -347,7 +348,12 @@ private static Type RemoveNonEssentialParts(Type type) current = GetInnerType(current); } - if (IsOptional(type)) + if (IsOptional(current)) + { + current = GetInnerType(current); + } + + if (IsConnectionResolver(current)) { current = GetInnerType(current); } @@ -390,9 +396,10 @@ private static Type GetInnerType(Type type) || IsNullableType(type) || IsWrapperType(type) || IsResolverResultType(type) - || IsOptional(type)) + || IsOptional(type) + || IsConnectionResolver(type)) { - return type.GetGenericArguments().First(); + return type.GetGenericArguments()[0]; } if (ImplementsListInterface(type)) @@ -455,6 +462,12 @@ private static bool IsSupportedCollectionInterface( return false; } + public static bool IsConnectionResolver(Type type) + { + return type.IsGenericType && + type.GetGenericTypeDefinition() == typeof(IConnectionResolver<>); + } + public static bool IsListType(Type type) { return type.IsArray From 4b3bc1a03f90042e25d7a8eab0ef9aa2f8e37bfa Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sat, 16 May 2020 01:55:09 +0200 Subject: [PATCH 09/15] Added more tests --- .../Types/Relay/ConnectionTypeTests.cs | 58 ++++++++++++++++++- .../Types/Configuration/SchemaTypeResolver.cs | 4 ++ src/Core/Types/Types/Relay/Connection.cs | 12 ++-- src/Core/Types/Types/Relay/IConnection.cs | 2 +- src/Core/Types/Types/Relay/PageInfo.cs | 2 +- 5 files changed, 68 insertions(+), 10 deletions(-) diff --git a/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs b/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs index 22547b398cc..ea25b2a647e 100644 --- a/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs +++ b/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using HotChocolate.Execution; +using HotChocolate.Resolvers; using Snapshooter.Xunit; using Xunit; @@ -163,6 +165,16 @@ public async Task UsePaging_WithComplexType() result.MatchSnapshot(); } + [Fact] + public void InferSchemaWithAttributesCorrectly() + { + SchemaBuilder.New() + .AddQueryType() + .Create() + .ToString() + .MatchSnapshot(); + } + public class QueryType : ObjectType { @@ -217,7 +229,7 @@ protected override void Configure( IObjectTypeDescriptor descriptor) { descriptor.Interface(); - descriptor.Field(t => t.Bar).UsePaging(); + descriptor.Field>(t => t.Bar).UsePaging(); } } @@ -238,5 +250,49 @@ public class Foo public ICollection Bar { get; } = new List { "a", "b", "c", "d", "e", "f", "g" }; } + + public class QueryWithPagingAttribute + { + [UsePaging] + public ICollection Collection { get; } = + new List { "a", "b", "c", "d", "e", "f", "g" }; + + [UsePaging] + public IQueryable Queryable { get; } = + new List { "a", "b", "c", "d", "e", "f", "g" }.AsQueryable(); + + public IEnumerable Enumerable { get; } = + new List { "a", "b", "c", "d", "e", "f", "g" }.AsQueryable(); + + public ConnectionOfString ConnectionOfString { get; } = + new ConnectionOfString(); + + [UsePaging] + public IConnectionResolver> IConnectionOfString { get; } = + new ConnectionOfString(); + } + + public class ConnectionOfString : IConnectionResolver> + { + public ValueTask ResolveAsync( + IMiddlewareContext context, + IEnumerable source, + ConnectionArguments arguments = default, + bool withTotalCount = false, + CancellationToken cancellationToken = default) + { + return new ValueTask(new Connection( + new PageInfo(false, false, "foo", "foo"), + new List> { new Edge("abc", "foo") })); + } + + public ValueTask ResolveAsync( + IMiddlewareContext context, + object source, + ConnectionArguments arguments = default, + bool withTotalCount = false, + CancellationToken cancellationToken = default) => + ResolveAsync(context, source, arguments, withTotalCount, cancellationToken); + } } } diff --git a/src/Core/Types/Configuration/SchemaTypeResolver.cs b/src/Core/Types/Configuration/SchemaTypeResolver.cs index c7e3f706103..715a019ae01 100644 --- a/src/Core/Types/Configuration/SchemaTypeResolver.cs +++ b/src/Core/Types/Configuration/SchemaTypeResolver.cs @@ -49,6 +49,10 @@ public static bool TryInferSchemaType( .MakeGenericType(unresolvedType.Type), unresolvedType.Context); } + else if (Scalars.TryGetScalar(unresolvedType.Type, out schemaType)) + { + return true; + } else { schemaType = null; diff --git a/src/Core/Types/Types/Relay/Connection.cs b/src/Core/Types/Types/Relay/Connection.cs index 1cc314ed445..f6a52a47db7 100644 --- a/src/Core/Types/Types/Relay/Connection.cs +++ b/src/Core/Types/Types/Relay/Connection.cs @@ -8,18 +8,16 @@ public class Connection { public Connection( IPageInfo pageInfo, - IReadOnlyCollection> edges) + IReadOnlyList> edges) { - PageInfo = pageInfo - ?? throw new ArgumentNullException(nameof(pageInfo)); - Edges = edges - ?? throw new ArgumentNullException(nameof(edges)); + PageInfo = pageInfo ?? throw new ArgumentNullException(nameof(pageInfo)); + Edges = edges ?? throw new ArgumentNullException(nameof(edges)); } public IPageInfo PageInfo { get; } - public IReadOnlyCollection> Edges { get; } + public IReadOnlyList> Edges { get; } - IReadOnlyCollection IConnection.Edges => Edges; + IReadOnlyList IConnection.Edges => Edges; } } diff --git a/src/Core/Types/Types/Relay/IConnection.cs b/src/Core/Types/Types/Relay/IConnection.cs index 34ad500f85a..2ec91488022 100644 --- a/src/Core/Types/Types/Relay/IConnection.cs +++ b/src/Core/Types/Types/Relay/IConnection.cs @@ -6,6 +6,6 @@ public interface IConnection { IPageInfo PageInfo { get; } - IReadOnlyCollection Edges { get; } + IReadOnlyList Edges { get; } } } diff --git a/src/Core/Types/Types/Relay/PageInfo.cs b/src/Core/Types/Types/Relay/PageInfo.cs index 4866ed64c28..186247374fd 100644 --- a/src/Core/Types/Types/Relay/PageInfo.cs +++ b/src/Core/Types/Types/Relay/PageInfo.cs @@ -7,7 +7,7 @@ public PageInfo( bool hasPreviousPage, string startCursor, string endCursor, - long? totalCount) + long? totalCount = null) { HasNextPage = hasNextPage; HasPreviousPage = hasPreviousPage; From 58d24610b706b78bddd858d1223768baa42f2aca Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sat, 16 May 2020 10:10:39 +0200 Subject: [PATCH 10/15] fixed schema inferrence --- .../Types/Relay/ConnectionTypeTests.cs | 1 + ...ts.InferSchemaWithAttributesCorrectly.snap | 53 +++++++++++++++++++ .../Types/Configuration/TypeDiscoverer.cs | 2 + .../Definitions/ObjectFieldDefinition.cs | 2 + .../Types/Descriptors/DescriptorBase~1.cs | 3 +- .../Descriptors/ObjectFieldDescriptor.cs | 30 ++++++++--- .../PagingObjectFieldDescriptorExtensions.cs | 18 +------ .../Types/Types/Relay/UsePagingAttribute.cs | 27 +++++++++- .../Types/Utilities/DotNetTypeInfoFactory.cs | 51 ++++++++++++++++-- src/Core/Types/Utilities/ReflectionUtils.cs | 2 +- 10 files changed, 160 insertions(+), 29 deletions(-) create mode 100644 src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.InferSchemaWithAttributesCorrectly.snap diff --git a/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs b/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs index ea25b2a647e..2031a0b0547 100644 --- a/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs +++ b/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs @@ -261,6 +261,7 @@ public class QueryWithPagingAttribute public IQueryable Queryable { get; } = new List { "a", "b", "c", "d", "e", "f", "g" }.AsQueryable(); + [UsePaging] public IEnumerable Enumerable { get; } = new List { "a", "b", "c", "d", "e", "f", "g" }.AsQueryable(); diff --git a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.InferSchemaWithAttributesCorrectly.snap b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.InferSchemaWithAttributesCorrectly.snap new file mode 100644 index 00000000000..18f19783e35 --- /dev/null +++ b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.InferSchemaWithAttributesCorrectly.snap @@ -0,0 +1,53 @@ +schema { + query: QueryWithPagingAttribute +} + +"Information about pagination in a connection." +type PageInfo { + "When paginating forwards, the cursor to continue." + endCursor: String + "Indicates whether more edges exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more edges exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! + "When paginating backwards, the cursor to continue." + startCursor: String +} + +type QueryWithPagingAttribute { + collection(after: String before: String first: PaginationAmount last: PaginationAmount): StringConnection + connectionOfString(after: String before: String first: PaginationAmount last: PaginationAmount): StringConnection + enumerable(after: String before: String first: PaginationAmount last: PaginationAmount): StringConnection + iConnectionOfString(after: String before: String first: PaginationAmount last: PaginationAmount): StringConnection + queryable(after: String before: String first: PaginationAmount last: PaginationAmount): StringConnection +} + +"A connection to a list of items." +type StringConnection { + "A list of edges." + edges: [StringEdge!] + "A flattened list of the nodes." + nodes: [String] + "Information to aid in pagination." + pageInfo: PageInfo! + totalCount: Int! +} + +"An edge in a connection." +type StringEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: String +} + +"The `Boolean` scalar type represents `true` or `false`." +scalar Boolean + +"The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1." +scalar Int + +scalar PaginationAmount + +"The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text." +scalar String diff --git a/src/Core/Types/Configuration/TypeDiscoverer.cs b/src/Core/Types/Configuration/TypeDiscoverer.cs index 29b22e87bf4..aca4bab4dca 100644 --- a/src/Core/Types/Configuration/TypeDiscoverer.cs +++ b/src/Core/Types/Configuration/TypeDiscoverer.cs @@ -66,6 +66,7 @@ public DiscoveredTypes DiscoverTypes() catch (SchemaException ex) { _errors.AddRange(ex.Errors); + break; } catch (Exception ex) { @@ -73,6 +74,7 @@ public DiscoveredTypes DiscoverTypes() .SetMessage(ex.Message) .SetException(ex) .Build()); + break; } } while (resolved && tries < max); diff --git a/src/Core/Types/Types/Descriptors/Definitions/ObjectFieldDefinition.cs b/src/Core/Types/Types/Descriptors/Definitions/ObjectFieldDefinition.cs index 77c80a0247c..d5f3656cd48 100644 --- a/src/Core/Types/Types/Descriptors/Definitions/ObjectFieldDefinition.cs +++ b/src/Core/Types/Types/Descriptors/Definitions/ObjectFieldDefinition.cs @@ -24,5 +24,7 @@ public class ObjectFieldDefinition public IList MiddlewareComponents { get; } = new List(); + + public bool IsPagingEnabled { get; set; } } } diff --git a/src/Core/Types/Types/Descriptors/DescriptorBase~1.cs b/src/Core/Types/Types/Descriptors/DescriptorBase~1.cs index 6924144cd83..3540b45859a 100644 --- a/src/Core/Types/Types/Descriptors/DescriptorBase~1.cs +++ b/src/Core/Types/Types/Descriptors/DescriptorBase~1.cs @@ -17,8 +17,7 @@ public abstract class DescriptorBase protected DescriptorBase(IDescriptorContext context) { - Context = context - ?? throw new ArgumentNullException(nameof(context)); + Context = context ?? throw new ArgumentNullException(nameof(context)); } protected internal IDescriptorContext Context { get; } diff --git a/src/Core/Types/Types/Descriptors/ObjectFieldDescriptor.cs b/src/Core/Types/Types/Descriptors/ObjectFieldDescriptor.cs index ed0fbf8885f..7f9bb8e2c2f 100644 --- a/src/Core/Types/Types/Descriptors/ObjectFieldDescriptor.cs +++ b/src/Core/Types/Types/Descriptors/ObjectFieldDescriptor.cs @@ -6,6 +6,7 @@ using HotChocolate.Properties; using HotChocolate.Resolvers; using HotChocolate.Types.Descriptors.Definitions; +using HotChocolate.Types.Relay; using HotChocolate.Utilities; #nullable enable @@ -60,8 +61,7 @@ protected ObjectFieldDescriptor( if (member is MethodInfo m) { - Parameters = m.GetParameters().ToDictionary( - t => new NameString(t.Name)); + Parameters = m.GetParameters().ToDictionary(t => new NameString(t.Name)); Definition.ResultType = m.ReturnType; } else if (member is PropertyInfo p) @@ -85,6 +85,20 @@ protected override void OnCreateDefinition( Definition.Member); } + Type resultType = definition.ResultType; + if (resultType == typeof(object)) + { + MemberInfo member = definition.ResolverMember ?? definition.Member; + resultType = member.GetReturnType(true) ?? typeof(object); + } + + if (typeof(IConnectionResolver).IsAssignableFrom(resultType) && + !definition.IsPagingEnabled) + { + var paging = new UsePagingAttribute(); + paging.TryConfigure(Context, this, resultType); + } + base.OnCreateDefinition(definition); CompleteArguments(definition); @@ -207,7 +221,7 @@ public IObjectFieldDescriptor Resolver( Definition.SetMoreSpecificType(resultType, TypeContext.Output); Type resultTypeDef = resultType.GetGenericTypeDefinition(); - Type clrResultType = resultType.IsGenericType + Type clrResultType = resultType.IsGenericType && resultTypeDef == typeof(NativeType<>) ? resultType.GetGenericArguments()[0] : resultType; @@ -216,6 +230,10 @@ public IObjectFieldDescriptor Resolver( { Definition.ResultType = clrResultType; } + else + { + Definition.ResultType = typeof(object); + } } return this; } @@ -238,7 +256,7 @@ public IObjectFieldDescriptor ResolveWith( Definition.ResolverType = typeof(TResolver); Definition.ResolverMember = member; Definition.Resolver = null; - Definition.ResultType = resultType; + Definition.ResultType = member.GetReturnType(true) ?? typeof(object); return this; } @@ -255,12 +273,12 @@ public IObjectFieldDescriptor ResolveWith( throw new ArgumentNullException(nameof(propertyOrMethod)); } - if (propertyOrMethod is PropertyInfo || propertyOrMethod is MethodInfo) + if (propertyOrMethod is PropertyInfo p || propertyOrMethod is MethodInfo m) { Definition.ResolverType = propertyOrMethod.DeclaringType; Definition.ResolverMember = propertyOrMethod; Definition.Resolver = null; - Definition.ResultType = propertyOrMethod.GetReturnType(); + Definition.ResultType = propertyOrMethod.GetReturnType(true) ?? typeof(object); } throw new ArgumentException( diff --git a/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs b/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs index 5e851f89790..ff738c5dc36 100644 --- a/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs +++ b/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using HotChocolate.Resolvers; using HotChocolate.Types.Descriptors; +using HotChocolate.Utilities; namespace HotChocolate.Types.Relay { @@ -46,7 +47,7 @@ private static IObjectFieldDescriptor UsePaging( MemberInfo member = defintion.ResolverMember ?? defintion.Member; Type resultType = defintion.Resolver is { } && defintion.ResultType is { } ? defintion.ResultType - : GetResultType(member); + : member.GetReturnType(true) ?? typeof(object); resultType = UnwrapType(resultType); FieldMiddleware middleware = CreateMiddleware(resultType, entityType); @@ -95,21 +96,6 @@ private static FieldMiddleware CreateMiddleware(Type sourceType, Type entityType return FieldClassMiddlewareFactory.Create(middlewareType); } - private static Type GetResultType(MemberInfo member) - { - if (member is PropertyInfo p) - { - return p.PropertyType; - } - - if (member is MethodInfo m) - { - return m.ReturnType; - } - - throw new NotSupportedException(); - } - internal static Type UnwrapType(Type resultType) { if (resultType.IsGenericType && diff --git a/src/Core/Types/Types/Relay/UsePagingAttribute.cs b/src/Core/Types/Types/Relay/UsePagingAttribute.cs index 2c59345510a..db6f8704ce0 100644 --- a/src/Core/Types/Types/Relay/UsePagingAttribute.cs +++ b/src/Core/Types/Types/Relay/UsePagingAttribute.cs @@ -50,12 +50,37 @@ protected internal override void TryConfigure( } } + internal void TryConfigure( + IDescriptorContext context, + IDescriptor descriptor, + Type resultType) + { + var typeReference = new ClrTypeReference(resultType, TypeContext.Output); + Type schemaType = GetSchemaType(context, typeReference); + + if (descriptor is IObjectFieldDescriptor ofd) + { + _off.MakeGenericMethod(schemaType).Invoke(null, new[] { ofd }); + } + else if (descriptor is IInterfaceFieldDescriptor ifd) + { + _iff.MakeGenericMethod(schemaType).Invoke(null, new[] { ifd }); + } + } + private Type GetSchemaType( IDescriptorContext context, MemberInfo member) { - Type? type = SchemaType; ITypeReference returnType = context.Inspector.GetReturnType(member, TypeContext.Output); + return GetSchemaType(context, returnType); + } + + private Type GetSchemaType( + IDescriptorContext context, + ITypeReference returnType) + { + Type? type = SchemaType; if (type is null && returnType is IClrTypeReference clr && diff --git a/src/Core/Types/Utilities/DotNetTypeInfoFactory.cs b/src/Core/Types/Utilities/DotNetTypeInfoFactory.cs index d553db86ef5..c4e12f7067c 100644 --- a/src/Core/Types/Utilities/DotNetTypeInfoFactory.cs +++ b/src/Core/Types/Utilities/DotNetTypeInfoFactory.cs @@ -396,8 +396,7 @@ private static Type GetInnerType(Type type) || IsNullableType(type) || IsWrapperType(type) || IsResolverResultType(type) - || IsOptional(type) - || IsConnectionResolver(type)) + || IsOptional(type)) { return type.GetGenericArguments()[0]; } @@ -407,6 +406,11 @@ private static Type GetInnerType(Type type) return GetInnerListType(type); } + if (IsConnectionResolver(type)) + { + return GetConnectionResolverSourceType(type); + } + return null; } @@ -464,10 +468,51 @@ private static bool IsSupportedCollectionInterface( public static bool IsConnectionResolver(Type type) { - return type.IsGenericType && + if (IsConnectionResolverInterface(type)) + { + return true; + } + + if (type.IsClass) + { + foreach (Type interfaceType in type.GetInterfaces()) + { + if (IsConnectionResolverInterface(interfaceType)) + { + return true; + } + } + } + + return false; + } + + public static bool IsConnectionResolverInterface(Type type) + { + return type.IsInterface && + type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IConnectionResolver<>); } + internal static Type GetConnectionResolverSourceType(Type type) + { + if (IsConnectionResolverInterface(type)) + { + return type.GetGenericArguments()[0]; + } + + + foreach (Type interfaceType in type.GetInterfaces()) + { + if (IsConnectionResolverInterface(interfaceType)) + { + return interfaceType.GetGenericArguments()[0]; + } + } + + return null; + } + public static bool IsListType(Type type) { return type.IsArray diff --git a/src/Core/Types/Utilities/ReflectionUtils.cs b/src/Core/Types/Utilities/ReflectionUtils.cs index b025f95698f..3e4188eae16 100644 --- a/src/Core/Types/Utilities/ReflectionUtils.cs +++ b/src/Core/Types/Utilities/ReflectionUtils.cs @@ -187,7 +187,7 @@ private static ITypeReference GetTypeReference( return null; } - public static Type GetReturnType(this MemberInfo member) + public static Type GetReturnType(this MemberInfo member, bool ignoreAttributes = false) { if (member.IsDefined(typeof(GraphQLTypeAttribute))) { From 7f106de79382c78e7c99622990160f51198e4287 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sat, 16 May 2020 10:14:09 +0200 Subject: [PATCH 11/15] tests for collection and queryable --- .../Types/Relay/ConnectionTypeTests.cs | 66 +++++++++++++++++++ ...sePagingAttribute_InMemory_Collection.snap | 43 ++++++++++++ ...UsePagingAttribute_InMemory_Queryable.snap | 43 ++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePagingAttribute_InMemory_Collection.snap create mode 100644 src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePagingAttribute_InMemory_Queryable.snap diff --git a/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs b/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs index 2031a0b0547..b093b3634c7 100644 --- a/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs +++ b/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs @@ -175,6 +175,72 @@ public void InferSchemaWithAttributesCorrectly() .MatchSnapshot(); } + [Fact] + public async Task UsePagingAttribute_InMemory_Collection() + { + // arrange + ISchema schema = SchemaBuilder.New() + .AddQueryType() + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + string query = @" + { + collection { + edges { + cursor + node + } + pageInfo + { + hasNextPage + } + totalCount + } + } + "; + + // act + IExecutionResult result = await executor.ExecuteAsync(query); + + // assert + result.MatchSnapshot(); + } + + [Fact] + public async Task UsePagingAttribute_InMemory_Queryable() + { + // arrange + ISchema schema = SchemaBuilder.New() + .AddQueryType() + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + string query = @" + { + queryable { + edges { + cursor + node + } + pageInfo + { + hasNextPage + } + totalCount + } + } + "; + + // act + IExecutionResult result = await executor.ExecuteAsync(query); + + // assert + result.MatchSnapshot(); + } + public class QueryType : ObjectType { diff --git a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePagingAttribute_InMemory_Collection.snap b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePagingAttribute_InMemory_Collection.snap new file mode 100644 index 00000000000..76bf898ad03 --- /dev/null +++ b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePagingAttribute_InMemory_Collection.snap @@ -0,0 +1,43 @@ +{ + "Data": { + "collection": { + "edges": [ + { + "cursor": "MA==", + "node": "a" + }, + { + "cursor": "MQ==", + "node": "b" + }, + { + "cursor": "Mg==", + "node": "c" + }, + { + "cursor": "Mw==", + "node": "d" + }, + { + "cursor": "NA==", + "node": "e" + }, + { + "cursor": "NQ==", + "node": "f" + }, + { + "cursor": "Ng==", + "node": "g" + } + ], + "pageInfo": { + "hasNextPage": false + }, + "totalCount": 7 + } + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePagingAttribute_InMemory_Queryable.snap b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePagingAttribute_InMemory_Queryable.snap new file mode 100644 index 00000000000..0caa3b47fd4 --- /dev/null +++ b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePagingAttribute_InMemory_Queryable.snap @@ -0,0 +1,43 @@ +{ + "Data": { + "queryable": { + "edges": [ + { + "cursor": "MA==", + "node": "a" + }, + { + "cursor": "MQ==", + "node": "b" + }, + { + "cursor": "Mg==", + "node": "c" + }, + { + "cursor": "Mw==", + "node": "d" + }, + { + "cursor": "NA==", + "node": "e" + }, + { + "cursor": "NQ==", + "node": "f" + }, + { + "cursor": "Ng==", + "node": "g" + } + ], + "pageInfo": { + "hasNextPage": false + }, + "totalCount": 7 + } + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} From 437ba824a810b98bc6f04da512ec64cf5f378cf1 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sat, 16 May 2020 10:15:08 +0200 Subject: [PATCH 12/15] added enumerable tests --- .../Types/Relay/ConnectionTypeTests.cs | 33 ++++++++++++++ ...sePagingAttribute_InMemory_Enumerable.snap | 43 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePagingAttribute_InMemory_Enumerable.snap diff --git a/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs b/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs index b093b3634c7..f22932eab67 100644 --- a/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs +++ b/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs @@ -241,6 +241,39 @@ public async Task UsePagingAttribute_InMemory_Queryable() result.MatchSnapshot(); } + [Fact] + public async Task UsePagingAttribute_InMemory_Enumerable() + { + // arrange + ISchema schema = SchemaBuilder.New() + .AddQueryType() + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + string query = @" + { + enumerable { + edges { + cursor + node + } + pageInfo + { + hasNextPage + } + totalCount + } + } + "; + + // act + IExecutionResult result = await executor.ExecuteAsync(query); + + // assert + result.MatchSnapshot(); + } + public class QueryType : ObjectType { diff --git a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePagingAttribute_InMemory_Enumerable.snap b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePagingAttribute_InMemory_Enumerable.snap new file mode 100644 index 00000000000..5ea3083b0d7 --- /dev/null +++ b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePagingAttribute_InMemory_Enumerable.snap @@ -0,0 +1,43 @@ +{ + "Data": { + "enumerable": { + "edges": [ + { + "cursor": "MA==", + "node": "a" + }, + { + "cursor": "MQ==", + "node": "b" + }, + { + "cursor": "Mg==", + "node": "c" + }, + { + "cursor": "Mw==", + "node": "d" + }, + { + "cursor": "NA==", + "node": "e" + }, + { + "cursor": "NQ==", + "node": "f" + }, + { + "cursor": "Ng==", + "node": "g" + } + ], + "pageInfo": { + "hasNextPage": false + }, + "totalCount": 7 + } + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} From 87392c796ca2826d1d17d23f0be2bfd68feb7b87 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sat, 16 May 2020 10:45:11 +0200 Subject: [PATCH 13/15] Added more tests --- .../Types/Relay/ConnectionTypeTests.cs | 87 ++++++++++++++++++- ...ng_Attribute_When_ReturnType_IsPaging.snap | 19 ++++ ...bute_With_Injected_ConnectionResolver.snap | 19 ++++ .../Contracts/IDescriptorExtension~1.cs | 2 + .../Types/Descriptors/DescriptorBase~1.cs | 8 ++ .../Types/Types/Relay/ConnectionMiddleware.cs | 4 +- .../PagingObjectFieldDescriptorExtensions.cs | 4 + 7 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.Infer_UsePaging_Attribute_When_ReturnType_IsPaging.snap create mode 100644 src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePagingAttribute_With_Injected_ConnectionResolver.snap diff --git a/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs b/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs index f22932eab67..c94880be24b 100644 --- a/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs +++ b/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs @@ -1,9 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using HotChocolate.Execution; using HotChocolate.Resolvers; +using Microsoft.Extensions.DependencyInjection; using Snapshooter.Xunit; using Xunit; @@ -274,6 +276,80 @@ public async Task UsePagingAttribute_InMemory_Enumerable() result.MatchSnapshot(); } + [Fact] + public async Task Infer_UsePaging_Attribute_When_ReturnType_IsPaging() + { + // arrange + ISchema schema = SchemaBuilder.New() + .AddQueryType() + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + string query = @" + { + connectionOfString { + edges { + cursor + node + } + pageInfo + { + hasNextPage + } + totalCount + } + } + "; + + // act + IExecutionResult result = await executor.ExecuteAsync(query); + + // assert + result.MatchSnapshot(); + } + + [Fact] + public async Task UsePagingAttribute_With_Injected_ConnectionResolver() + { + // arrange + ISchema schema = SchemaBuilder.New() + .AddQueryType() + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + string query = @" + { + enumerable { + edges { + cursor + node + } + pageInfo + { + hasNextPage + } + totalCount + } + }"; + + IServiceProvider services = new ServiceCollection() + .AddSingleton>, ConnectionOfString>() + .BuildServiceProvider(); + + IReadOnlyQueryRequest request = QueryRequestBuilder.New() + .SetQuery(query) + .SetServices(services) + .Create(); + + // act + IExecutionResult result = await executor.ExecuteAsync(request); + + // assert + result.MatchSnapshot(); + } + public class QueryType : ObjectType { @@ -382,7 +458,7 @@ public ValueTask ResolveAsync( CancellationToken cancellationToken = default) { return new ValueTask(new Connection( - new PageInfo(false, false, "foo", "foo"), + new PageInfo(false, false, "foo", "foo", 1), new List> { new Edge("abc", "foo") })); } @@ -392,7 +468,12 @@ public ValueTask ResolveAsync( ConnectionArguments arguments = default, bool withTotalCount = false, CancellationToken cancellationToken = default) => - ResolveAsync(context, source, arguments, withTotalCount, cancellationToken); + ResolveAsync( + context, + (IEnumerable)source, + arguments, + withTotalCount, + cancellationToken); } } } diff --git a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.Infer_UsePaging_Attribute_When_ReturnType_IsPaging.snap b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.Infer_UsePaging_Attribute_When_ReturnType_IsPaging.snap new file mode 100644 index 00000000000..7376bea0704 --- /dev/null +++ b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.Infer_UsePaging_Attribute_When_ReturnType_IsPaging.snap @@ -0,0 +1,19 @@ +{ + "Data": { + "connectionOfString": { + "edges": [ + { + "cursor": "foo", + "node": "abc" + } + ], + "pageInfo": { + "hasNextPage": false + }, + "totalCount": 1 + } + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePagingAttribute_With_Injected_ConnectionResolver.snap b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePagingAttribute_With_Injected_ConnectionResolver.snap new file mode 100644 index 00000000000..d236bf791ab --- /dev/null +++ b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePagingAttribute_With_Injected_ConnectionResolver.snap @@ -0,0 +1,19 @@ +{ + "Data": { + "enumerable": { + "edges": [ + { + "cursor": "foo", + "node": "abc" + } + ], + "pageInfo": { + "hasNextPage": false + }, + "totalCount": 1 + } + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Types/Types/Descriptors/Contracts/IDescriptorExtension~1.cs b/src/Core/Types/Types/Descriptors/Contracts/IDescriptorExtension~1.cs index 59ba90f96a6..bb2b021cb6b 100644 --- a/src/Core/Types/Types/Descriptors/Contracts/IDescriptorExtension~1.cs +++ b/src/Core/Types/Types/Descriptors/Contracts/IDescriptorExtension~1.cs @@ -9,6 +9,8 @@ namespace HotChocolate.Types public interface IDescriptorExtension where T : DefinitionBase { + void ModifyDefinition(Action modify); + void OnBeforeCreate(Action configure); INamedDependencyDescriptor OnBeforeNaming( diff --git a/src/Core/Types/Types/Descriptors/DescriptorBase~1.cs b/src/Core/Types/Types/Descriptors/DescriptorBase~1.cs index 3540b45859a..44a464749e7 100644 --- a/src/Core/Types/Types/Descriptors/DescriptorBase~1.cs +++ b/src/Core/Types/Types/Descriptors/DescriptorBase~1.cs @@ -50,6 +50,14 @@ protected virtual void OnCreateDefinition(T definition) DefinitionBase IDefinitionFactory.CreateDefinition() => CreateDefinition(); + void IDescriptorExtension.ModifyDefinition(Action modify) => + ModifyDefinition(modify); + + private void ModifyDefinition(Action modify) + { + modify(Definition); + } + void IDescriptorExtension.OnBeforeCreate(Action configure) => OnBeforeCreate(configure); diff --git a/src/Core/Types/Types/Relay/ConnectionMiddleware.cs b/src/Core/Types/Types/Relay/ConnectionMiddleware.cs index dd5026129f6..675594895c8 100644 --- a/src/Core/Types/Types/Relay/ConnectionMiddleware.cs +++ b/src/Core/Types/Types/Relay/ConnectionMiddleware.cs @@ -32,9 +32,9 @@ public async Task InvokeAsync( if (context.Result is IConnectionResolver localConnectionResolver) { - context.Result = localConnectionResolver.ResolveAsync( + context.Result = await localConnectionResolver.ResolveAsync( context, - context.Result, + default, // in this case we do not have a result arguments, true, // where should we store this? context.RequestAborted) diff --git a/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs b/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs index ff738c5dc36..3a4f9569f91 100644 --- a/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs +++ b/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs @@ -28,6 +28,10 @@ private static IObjectFieldDescriptor UsePaging( { FieldMiddleware placeholder = next => context => Task.CompletedTask; + descriptor + .Extend() + .ModifyDefinition(d => d.IsPagingEnabled = true); + descriptor .AddPagingArguments() .Type>() From 5adc9fb9384bb2618ac0d1ca517af6f467ef4c3c Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 18 May 2020 10:33:00 +0200 Subject: [PATCH 14/15] Fixed tests --- .../SelectionTestsBase.cs | 2 +- .../Types/Relay/ConnectionTypeTests.cs | 17 +++--- ...ectionType_Without_Paging_Middleware.snap} | 0 ...ts.InferSchemaWithAttributesCorrectly.snap | 3 +- .../Contracts/IDescriptorExtension~1.cs | 2 - .../Definitions/ObjectFieldDefinition.cs | 2 - .../Types/Descriptors/DescriptorBase~1.cs | 8 --- .../Descriptors/ObjectFieldDescriptor.cs | 14 ----- .../Types/Types/Relay/ConnectionMiddleware.cs | 12 +--- .../PagingObjectFieldDescriptorExtensions.cs | 4 -- src/Core/Types/Types/Relay/IEdge.cs | 7 +++ .../Types/Types/Relay/UsePagingAttribute.cs | 18 ------ .../Types/Utilities/DotNetTypeInfoFactory.cs | 59 +------------------ 13 files changed, 21 insertions(+), 127 deletions(-) rename src/Core/Types.Tests/Types/Relay/__snapshots__/{ConnectionTypeTests.Infer_UsePaging_Attribute_When_ReturnType_IsPaging.snap => ConnectionTypeTests.ConnectionType_Without_Paging_Middleware.snap} (100%) diff --git a/src/Core/Types.Selection.Abstractions.Tests/SelectionTestsBase.cs b/src/Core/Types.Selection.Abstractions.Tests/SelectionTestsBase.cs index 93fec90d2f4..9220e7c35bd 100644 --- a/src/Core/Types.Selection.Abstractions.Tests/SelectionTestsBase.cs +++ b/src/Core/Types.Selection.Abstractions.Tests/SelectionTestsBase.cs @@ -1106,7 +1106,7 @@ public virtual void Execute_Selection_Paging_OnlyMeta() var pageInfoResult = foosResult["pageInfo"] as IDictionary; Assert.NotNull(pageInfoResult); - Assert.Equal("eyJfX3RvdGFsQ291bnQiOjIsIl9fcG9zaXRpb24iOjB9", + Assert.Equal("MA==", pageInfoResult["startCursor"]); Assert.NotNull(resultCtx); diff --git a/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs b/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs index c94880be24b..5b53d26f622 100644 --- a/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs +++ b/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs @@ -277,7 +277,7 @@ public async Task UsePagingAttribute_InMemory_Enumerable() } [Fact] - public async Task Infer_UsePaging_Attribute_When_ReturnType_IsPaging() + public async Task ConnectionType_Without_Paging_Middleware() { // arrange ISchema schema = SchemaBuilder.New() @@ -440,12 +440,15 @@ public class QueryWithPagingAttribute public IEnumerable Enumerable { get; } = new List { "a", "b", "c", "d", "e", "f", "g" }.AsQueryable(); - public ConnectionOfString ConnectionOfString { get; } = - new ConnectionOfString(); - - [UsePaging] - public IConnectionResolver> IConnectionOfString { get; } = - new ConnectionOfString(); + [GraphQLType(typeof(ConnectionWithCountType))] + public Connection ConnectionOfString( + int? first = null, + int? last = null, + string? after = null, + string? before = null) => + new Connection( + new PageInfo(false, false, "foo", "foo", 1), + new List> { new Edge("abc", "foo") }); } public class ConnectionOfString : IConnectionResolver> diff --git a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.Infer_UsePaging_Attribute_When_ReturnType_IsPaging.snap b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.ConnectionType_Without_Paging_Middleware.snap similarity index 100% rename from src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.Infer_UsePaging_Attribute_When_ReturnType_IsPaging.snap rename to src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.ConnectionType_Without_Paging_Middleware.snap diff --git a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.InferSchemaWithAttributesCorrectly.snap b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.InferSchemaWithAttributesCorrectly.snap index 18f19783e35..a3f75ca9f27 100644 --- a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.InferSchemaWithAttributesCorrectly.snap +++ b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.InferSchemaWithAttributesCorrectly.snap @@ -16,9 +16,8 @@ type PageInfo { type QueryWithPagingAttribute { collection(after: String before: String first: PaginationAmount last: PaginationAmount): StringConnection - connectionOfString(after: String before: String first: PaginationAmount last: PaginationAmount): StringConnection + connectionOfString(after: String before: String first: Int last: Int): StringConnection enumerable(after: String before: String first: PaginationAmount last: PaginationAmount): StringConnection - iConnectionOfString(after: String before: String first: PaginationAmount last: PaginationAmount): StringConnection queryable(after: String before: String first: PaginationAmount last: PaginationAmount): StringConnection } diff --git a/src/Core/Types/Types/Descriptors/Contracts/IDescriptorExtension~1.cs b/src/Core/Types/Types/Descriptors/Contracts/IDescriptorExtension~1.cs index bb2b021cb6b..59ba90f96a6 100644 --- a/src/Core/Types/Types/Descriptors/Contracts/IDescriptorExtension~1.cs +++ b/src/Core/Types/Types/Descriptors/Contracts/IDescriptorExtension~1.cs @@ -9,8 +9,6 @@ namespace HotChocolate.Types public interface IDescriptorExtension where T : DefinitionBase { - void ModifyDefinition(Action modify); - void OnBeforeCreate(Action configure); INamedDependencyDescriptor OnBeforeNaming( diff --git a/src/Core/Types/Types/Descriptors/Definitions/ObjectFieldDefinition.cs b/src/Core/Types/Types/Descriptors/Definitions/ObjectFieldDefinition.cs index d5f3656cd48..77c80a0247c 100644 --- a/src/Core/Types/Types/Descriptors/Definitions/ObjectFieldDefinition.cs +++ b/src/Core/Types/Types/Descriptors/Definitions/ObjectFieldDefinition.cs @@ -24,7 +24,5 @@ public class ObjectFieldDefinition public IList MiddlewareComponents { get; } = new List(); - - public bool IsPagingEnabled { get; set; } } } diff --git a/src/Core/Types/Types/Descriptors/DescriptorBase~1.cs b/src/Core/Types/Types/Descriptors/DescriptorBase~1.cs index 44a464749e7..3540b45859a 100644 --- a/src/Core/Types/Types/Descriptors/DescriptorBase~1.cs +++ b/src/Core/Types/Types/Descriptors/DescriptorBase~1.cs @@ -50,14 +50,6 @@ protected virtual void OnCreateDefinition(T definition) DefinitionBase IDefinitionFactory.CreateDefinition() => CreateDefinition(); - void IDescriptorExtension.ModifyDefinition(Action modify) => - ModifyDefinition(modify); - - private void ModifyDefinition(Action modify) - { - modify(Definition); - } - void IDescriptorExtension.OnBeforeCreate(Action configure) => OnBeforeCreate(configure); diff --git a/src/Core/Types/Types/Descriptors/ObjectFieldDescriptor.cs b/src/Core/Types/Types/Descriptors/ObjectFieldDescriptor.cs index 7f9bb8e2c2f..6aa9f3b78d5 100644 --- a/src/Core/Types/Types/Descriptors/ObjectFieldDescriptor.cs +++ b/src/Core/Types/Types/Descriptors/ObjectFieldDescriptor.cs @@ -85,20 +85,6 @@ protected override void OnCreateDefinition( Definition.Member); } - Type resultType = definition.ResultType; - if (resultType == typeof(object)) - { - MemberInfo member = definition.ResolverMember ?? definition.Member; - resultType = member.GetReturnType(true) ?? typeof(object); - } - - if (typeof(IConnectionResolver).IsAssignableFrom(resultType) && - !definition.IsPagingEnabled) - { - var paging = new UsePagingAttribute(); - paging.TryConfigure(Context, this, resultType); - } - base.OnCreateDefinition(definition); CompleteArguments(definition); diff --git a/src/Core/Types/Types/Relay/ConnectionMiddleware.cs b/src/Core/Types/Types/Relay/ConnectionMiddleware.cs index 675594895c8..cfc26ee276f 100644 --- a/src/Core/Types/Types/Relay/ConnectionMiddleware.cs +++ b/src/Core/Types/Types/Relay/ConnectionMiddleware.cs @@ -30,17 +30,7 @@ public async Task InvokeAsync( context.Argument("after"), context.Argument("before")); - if (context.Result is IConnectionResolver localConnectionResolver) - { - context.Result = await localConnectionResolver.ResolveAsync( - context, - default, // in this case we do not have a result - arguments, - true, // where should we store this? - context.RequestAborted) - .ConfigureAwait(false); - } - else if (connectionResolver is { } && context.Result is TSource source) + if (connectionResolver is { } && context.Result is TSource source) { context.Result = await connectionResolver.ResolveAsync( context, diff --git a/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs b/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs index 3a4f9569f91..ff738c5dc36 100644 --- a/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs +++ b/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs @@ -28,10 +28,6 @@ private static IObjectFieldDescriptor UsePaging( { FieldMiddleware placeholder = next => context => Task.CompletedTask; - descriptor - .Extend() - .ModifyDefinition(d => d.IsPagingEnabled = true); - descriptor .AddPagingArguments() .Type>() diff --git a/src/Core/Types/Types/Relay/IEdge.cs b/src/Core/Types/Types/Relay/IEdge.cs index 9bf8a9e3e32..942f1668857 100644 --- a/src/Core/Types/Types/Relay/IEdge.cs +++ b/src/Core/Types/Types/Relay/IEdge.cs @@ -2,7 +2,14 @@ { public interface IEdge { + /// + /// Gets the cursor which identifies the in the current data set. + /// string Cursor { get; } + + /// + /// Gets the node. + /// object Node { get; } } } diff --git a/src/Core/Types/Types/Relay/UsePagingAttribute.cs b/src/Core/Types/Types/Relay/UsePagingAttribute.cs index db6f8704ce0..c60c7b50a0a 100644 --- a/src/Core/Types/Types/Relay/UsePagingAttribute.cs +++ b/src/Core/Types/Types/Relay/UsePagingAttribute.cs @@ -50,24 +50,6 @@ protected internal override void TryConfigure( } } - internal void TryConfigure( - IDescriptorContext context, - IDescriptor descriptor, - Type resultType) - { - var typeReference = new ClrTypeReference(resultType, TypeContext.Output); - Type schemaType = GetSchemaType(context, typeReference); - - if (descriptor is IObjectFieldDescriptor ofd) - { - _off.MakeGenericMethod(schemaType).Invoke(null, new[] { ofd }); - } - else if (descriptor is IInterfaceFieldDescriptor ifd) - { - _iff.MakeGenericMethod(schemaType).Invoke(null, new[] { ifd }); - } - } - private Type GetSchemaType( IDescriptorContext context, MemberInfo member) diff --git a/src/Core/Types/Utilities/DotNetTypeInfoFactory.cs b/src/Core/Types/Utilities/DotNetTypeInfoFactory.cs index c4e12f7067c..13b87818310 100644 --- a/src/Core/Types/Utilities/DotNetTypeInfoFactory.cs +++ b/src/Core/Types/Utilities/DotNetTypeInfoFactory.cs @@ -352,12 +352,7 @@ private static Type RemoveNonEssentialParts(Type type) { current = GetInnerType(current); } - - if (IsConnectionResolver(current)) - { - current = GetInnerType(current); - } - + return current; } @@ -406,11 +401,6 @@ private static Type GetInnerType(Type type) return GetInnerListType(type); } - if (IsConnectionResolver(type)) - { - return GetConnectionResolverSourceType(type); - } - return null; } @@ -466,53 +456,6 @@ private static bool IsSupportedCollectionInterface( return false; } - public static bool IsConnectionResolver(Type type) - { - if (IsConnectionResolverInterface(type)) - { - return true; - } - - if (type.IsClass) - { - foreach (Type interfaceType in type.GetInterfaces()) - { - if (IsConnectionResolverInterface(interfaceType)) - { - return true; - } - } - } - - return false; - } - - public static bool IsConnectionResolverInterface(Type type) - { - return type.IsInterface && - type.IsGenericType && - type.GetGenericTypeDefinition() == typeof(IConnectionResolver<>); - } - - internal static Type GetConnectionResolverSourceType(Type type) - { - if (IsConnectionResolverInterface(type)) - { - return type.GetGenericArguments()[0]; - } - - - foreach (Type interfaceType in type.GetInterfaces()) - { - if (IsConnectionResolverInterface(interfaceType)) - { - return interfaceType.GetGenericArguments()[0]; - } - } - - return null; - } - public static bool IsListType(Type type) { return type.IsArray From 6c9da662c3dc8e09eb9f3aadddee2d856baca630 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 18 May 2020 10:44:41 +0200 Subject: [PATCH 15/15] formatting --- .../Types.Selection.Abstractions.Tests/SelectionTestsBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Types.Selection.Abstractions.Tests/SelectionTestsBase.cs b/src/Core/Types.Selection.Abstractions.Tests/SelectionTestsBase.cs index 9220e7c35bd..6b387941014 100644 --- a/src/Core/Types.Selection.Abstractions.Tests/SelectionTestsBase.cs +++ b/src/Core/Types.Selection.Abstractions.Tests/SelectionTestsBase.cs @@ -1094,7 +1094,7 @@ public virtual void Execute_Selection_Paging_OnlyMeta() // act var result = executor.Execute("{ foos { totalCount pageInfo {startCursor}}}") - as IReadOnlyQueryResult; + as IReadOnlyQueryResult; // assert Assert.NotNull(result);